From 7d22cc102909c22b8702c7c4bd12dcabb736822b Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 4 Jul 2025 22:18:18 -0400 Subject: [PATCH] checkpoint kiri module migration work --- app.js | 41 +- src2/cli/kiri-cam-device.json | 26 + src2/cli/kiri-cam-process.json | 121 + src2/cli/kiri-cam-tools.json | 98 + src2/cli/kiri-controller.json | 4 + src2/cli/kiri-fdm-device.json | 62 + src2/cli/kiri-fdm-process.json | 69 + src2/cli/kiri-source.json | 59 + src2/data/index.js | 23 +- src2/data/local.js | 62 +- src2/dock/Dockerfile | 14 + src2/dock/compose.yml | 10 + src2/geo/paths.js | 92 + src2/geo/point.js | 13 +- src2/geo/points.js | 3 +- src2/geo/polygon.js | 7 +- src2/geo/polygons.js | 9 +- src2/kiri-dev/cam/Any.Generic.Grbl.json | 22 + src2/kiri-dev/cam/Any.Generic.LinuxCNC.json | 24 + src2/kiri-dev/cam/Any.Generic.TinyG.json | 22 + src2/kiri-dev/cam/Carbide3D.Nomad.883Pro.json | 22 + src2/kiri-dev/cam/Carbide3D.Shapeoko.3.json | 22 + src2/kiri-dev/cam/Carbide3D.Shapeoko.XL.json | 22 + src2/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json | 22 + src2/kiri-dev/cam/Genmitsu.3018.json | 24 + src2/kiri-dev/cam/Genmitsu.PROVerXL.4030.json | 24 + src2/kiri-dev/cam/Inventables.Carvey.Any.json | 24 + .../cam/Inventables.XCarve.1000mm.json | 22 + .../cam/Inventables.XCarve.500mm.json | 22 + src2/kiri-dev/cam/Langmuir.MR-1.json | 32 + src2/kiri-dev/cam/Makera.Carvera.json | 179 ++ src2/kiri-dev/cam/PrintNC.GrblHAL.json | 30 + .../cam/RatRig.StrongHold.ONE.750x750mm.json | 30 + src2/kiri-dev/cam/Roland.MDX-50.json | 22 + src2/kiri-dev/cam/ShopBot.Basic.json | 45 + src2/kiri-dev/cam/Sienci.AltMill.json | 30 + src2/kiri-dev/cam/Sienci.LongMill.MK2.json | 27 + src2/kiri-dev/cam/Sienci.LongMill.json | 30 + src2/kiri-dev/cam/Sienci.Mill.One.json | 27 + src2/kiri-dev/cam/Snapmaker.Original.json | 27 + src2/kiri-dev/cam/Techno.Davinci.CNC.json | 22 + src2/kiri-dev/cam/Tormach.24R.json | 32 + .../kiri-dev/cam/V1Engineering.Lowrider3.json | 24 + src2/kiri-dev/cam/V1Engineering.MPCNC.json | 24 + src2/kiri-dev/drag/Any.Generic.DragKnife.json | 21 + src2/kiri-dev/fdm/Any.Generic.Klipper.json | 46 + src2/kiri-dev/fdm/Any.Generic.Marlin.json | 46 + src2/kiri-dev/fdm/Anycubic.Chiron.json | 55 + src2/kiri-dev/fdm/Anycubic.Kobra.2.json | 184 ++ src2/kiri-dev/fdm/Anycubic.Kobra.json | 79 + src2/kiri-dev/fdm/Anycubic.Predator.json | 56 + src2/kiri-dev/fdm/Anycubic.i3.Mega.json | 51 + src2/kiri-dev/fdm/Atom.v2.json | 27 + src2/kiri-dev/fdm/Bambu.A1.json | 816 ++++++ src2/kiri-dev/fdm/Bambu.P1S.json | 502 ++++ src2/kiri-dev/fdm/Creality.CR-10S-Pro.json | 51 + src2/kiri-dev/fdm/Creality.CR-10S.json | 50 + src2/kiri-dev/fdm/Creality.CR-30.json | 172 ++ src2/kiri-dev/fdm/Creality.Ender.3.json | 153 + src2/kiri-dev/fdm/Creality.Ender.5.json | 38 + src2/kiri-dev/fdm/Creality.Ender.6.json | 38 + src2/kiri-dev/fdm/Creality.K1.json | 51 + src2/kiri-dev/fdm/Creality.Sermoon.D1.json | 152 + src2/kiri-dev/fdm/Cubicon.Single+.json | 37 + src2/kiri-dev/fdm/Cubicon.Style.json | 32 + src2/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json | 166 ++ src2/kiri-dev/fdm/DeltaMaker.2T.json | 31 + src2/kiri-dev/fdm/DeltaMaker.Original.json | 31 + src2/kiri-dev/fdm/FLSUN.QQ-S.json | 40 + src2/kiri-dev/fdm/Flashforge.Finder.json | 29 + src2/kiri-dev/fdm/Flashforge.Guider.II.json | 43 + src2/kiri-dev/fdm/FolgerTech.FT5.json | 39 + src2/kiri-dev/fdm/GridBot.One.json | 43 + src2/kiri-dev/fdm/GridBot.Two.json | 43 + .../kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json | 368 +++ .../kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json | 368 +++ .../kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json | 369 +++ .../kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json | 369 +++ .../kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json | 374 +++ src2/kiri-dev/fdm/Leapfrog.CreatrHS.json | 35 + src2/kiri-dev/fdm/MakerGear.M2.json | 37 + src2/kiri-dev/fdm/Makerbot.Replicator2.json | 44 + src2/kiri-dev/fdm/My.Bambu.P1S | 535 ++++ src2/kiri-dev/fdm/Prusa.i3.MK2S.json | 317 ++ src2/kiri-dev/fdm/Prusa.i3.MK3S+.json | 325 +++ src2/kiri-dev/fdm/Prusa.mini.json | 164 ++ src2/kiri-dev/fdm/Raise3D.N2.json | 65 + src2/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json | 312 ++ src2/kiri-dev/fdm/SainSmart.Infi-20.json | 165 ++ src2/kiri-dev/fdm/Snapmaker.Original.json | 38 + src2/kiri-dev/fdm/Tina2.Basic.json | 176 ++ src2/kiri-dev/fdm/TypeA.Series1.json | 37 + src2/kiri-dev/fdm/TypeA.Series1Pro.json | 40 + src2/kiri-dev/fdm/ULIO.3D.BRIX.json | 144 + src2/kiri-dev/fdm/ULIO.3D.STEAM.json | 146 + src2/kiri-dev/fdm/Ultimaker.Ultimaker2.json | 40 + src2/kiri-dev/fdm/Wanhao.Duplicator_4s.json | 33 + src2/kiri-dev/fdm/Wanhao.Duplicator_5s.json | 33 + src2/kiri-dev/fdm/Wanhao.Duplicator_6.json | 33 + src2/kiri-dev/fdm/Wanhao.Duplicator_i3.json | 33 + src2/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json | 31 + src2/kiri-dev/fdm/XYZ.daVinci.Mini.w.json | 32 + src2/kiri-dev/laser/Any.Generic.Laser.json | 21 + src2/kiri-dev/laser/Glowforge.json | 12 + src2/kiri-dev/laser/Snapmaker.A250T.json | 35 + src2/kiri-dev/laser/Snapmaker.A350T.json | 35 + src2/kiri-dev/laser/Snapmaker.Original.json | 35 + src2/kiri-dev/laser/Xtool.P2.json | 12 + src2/kiri-dev/laser/xTool.D1.json | 12 + src2/kiri-dev/laser/xTool.F1.Ultra.json | 12 + src2/kiri-dev/laser/xTool.F1.json | 12 + src2/kiri-dev/laser/xTool.M1.json | 12 + src2/kiri-dev/laser/xTool.P2S.json | 12 + src2/kiri-dev/laser/xTool.S1.json | 12 + src2/kiri-dev/sla/Anycubic.Photon.S.json | 10 + src2/kiri-dev/sla/Anycubic.Photon.json | 12 + src2/kiri-dev/sla/Creality.Halot.Max.json | 12 + src2/kiri-dev/sla/Creality.Halot.One.json | 12 + src2/kiri-dev/sla/Creality.Halot.Sky.json | 12 + src2/kiri-dev/wedm/Any.Generic.WireEDM.json | 53 + .../kiri-dev/wedm/RackRobo.Betta.Wire.V1.json | 53 + src2/kiri-dev/wjet/Any.Generic.WaterJet.json | 21 + src2/kiri-dev/wjet/HydroBLADE.json | 53 + src2/kiri-mode/cam/anim-2d-be.js | 310 ++ src2/kiri-mode/cam/anim-2d-fe.js | 309 ++ src2/kiri-mode/cam/anim-3d-be.js | 391 +++ src2/kiri-mode/cam/anim-3d-fe.js | 253 ++ src2/kiri-mode/cam/client.js | 2305 +++++++++++++++ src2/kiri-mode/cam/driver-be.js | 98 + src2/kiri-mode/cam/driver-fe.js | 89 + src2/kiri-mode/cam/export.js | 518 ++++ src2/kiri-mode/cam/ops.js | 1954 +++++++++++++ src2/kiri-mode/cam/prepare.js | 1099 +++++++ src2/kiri-mode/cam/slice.js | 739 +++++ src2/kiri-mode/cam/slicer.js | 510 ++++ src2/kiri-mode/cam/slicer2.js | 324 +++ src2/kiri-mode/cam/tool.js | 208 ++ src2/kiri-mode/cam/tools.js | 354 +++ src2/kiri-mode/cam/topo3.js | 894 ++++++ src2/kiri-mode/cam/topo4.js | 534 ++++ src2/kiri-mode/drag/driver.js | 11 + src2/kiri-mode/fdm/client.js | 627 ++++ src2/kiri-mode/fdm/driver.js | 83 + src2/kiri-mode/fdm/export.js | 979 +++++++ src2/kiri-mode/fdm/fill.js | 325 +++ src2/kiri-mode/fdm/post.js | 321 +++ src2/kiri-mode/fdm/prepare.js | 1514 ++++++++++ src2/kiri-mode/fdm/slice.js | 1908 ++++++++++++ src2/kiri-mode/laser/driver.js | 857 ++++++ src2/kiri-mode/sla/client.js | 88 + src2/kiri-mode/sla/driver.js | 42 + src2/kiri-mode/sla/export.js | 172 ++ src2/kiri-mode/sla/slice.js | 704 +++++ src2/kiri-mode/sla/x_cxdlp.js | 452 +++ src2/kiri-mode/sla/x_photon.js | 540 ++++ src2/kiri-mode/wedm/driver.js | 129 + src2/kiri-mode/wjet/driver.js | 11 + src2/kiri-run/cli.js | 227 ++ src2/kiri-run/engine.js | 222 ++ src2/kiri-run/frame.js | 89 + src2/kiri-run/minion.js | 194 ++ src2/kiri-run/worker.js | 750 +++++ src2/main/kiri.js | 22 + src2/mesh/work.js | 3 - src2/moto/license.js | 1 + src2/webpack/esbuild.config.mjs | 56 +- web2/kiri/cxdlp.html | 146 + web2/kiri/engine.html | 369 +++ web2/kiri/favicon-mobile.png | Bin 0 -> 6142 bytes web2/kiri/favicon.ico | Bin 0 -> 1150 bytes web2/kiri/frame.html | 164 ++ web2/kiri/gsbox.png | Bin 0 -> 65184 bytes web2/kiri/gyroid-test.html | 505 ++++ web2/kiri/index.css | 2563 +++++++++++++++++ web2/kiri/index.html | 752 +++++ web2/kiri/lang/da-dk.js | 1 + web2/kiri/lang/da.js | 649 +++++ web2/kiri/lang/de-de.js | 640 ++++ web2/kiri/lang/de.js | 1 + web2/kiri/lang/en-us.js | 1 + web2/kiri/lang/en.js | 932 ++++++ web2/kiri/lang/es-es.js | 650 +++++ web2/kiri/lang/es.js | 1 + web2/kiri/lang/fr-fr.js | 1 + web2/kiri/lang/fr.js | 642 +++++ web2/kiri/lang/pl-pl.js | 645 +++++ web2/kiri/lang/pl.js | 1 + web2/kiri/lang/pt-pt.js | 1 + web2/kiri/lang/pt.js | 650 +++++ web2/kiri/lang/zh.js | 649 +++++ web2/kiri/logo-cube-128.png | Bin 0 -> 22399 bytes web2/kiri/logo-cube-144.png | Bin 0 -> 25380 bytes web2/kiri/logo-cube-256.png | Bin 0 -> 48110 bytes web2/kiri/logo-cube-512.png | Bin 0 -> 104488 bytes web2/kiri/logo-cube-64.png | Bin 0 -> 11061 bytes web2/kiri/manifest.json | 15 + web2/kiri/style-alt.css | 114 + web2/kiri/style-atom3dp.css | 32 + web2/obj/box.stl | Bin 0 -> 684 bytes web2/obj/cube.stl | Bin 0 -> 16084 bytes web2/obj/pocket-test.stl | 198 ++ 201 files changed, 41263 insertions(+), 86 deletions(-) create mode 100644 src2/cli/kiri-cam-device.json create mode 100644 src2/cli/kiri-cam-process.json create mode 100644 src2/cli/kiri-cam-tools.json create mode 100644 src2/cli/kiri-controller.json create mode 100644 src2/cli/kiri-fdm-device.json create mode 100644 src2/cli/kiri-fdm-process.json create mode 100644 src2/cli/kiri-source.json create mode 100644 src2/dock/Dockerfile create mode 100644 src2/dock/compose.yml create mode 100644 src2/kiri-dev/cam/Any.Generic.Grbl.json create mode 100644 src2/kiri-dev/cam/Any.Generic.LinuxCNC.json create mode 100644 src2/kiri-dev/cam/Any.Generic.TinyG.json create mode 100644 src2/kiri-dev/cam/Carbide3D.Nomad.883Pro.json create mode 100644 src2/kiri-dev/cam/Carbide3D.Shapeoko.3.json create mode 100644 src2/kiri-dev/cam/Carbide3D.Shapeoko.XL.json create mode 100644 src2/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json create mode 100644 src2/kiri-dev/cam/Genmitsu.3018.json create mode 100644 src2/kiri-dev/cam/Genmitsu.PROVerXL.4030.json create mode 100644 src2/kiri-dev/cam/Inventables.Carvey.Any.json create mode 100644 src2/kiri-dev/cam/Inventables.XCarve.1000mm.json create mode 100644 src2/kiri-dev/cam/Inventables.XCarve.500mm.json create mode 100644 src2/kiri-dev/cam/Langmuir.MR-1.json create mode 100644 src2/kiri-dev/cam/Makera.Carvera.json create mode 100644 src2/kiri-dev/cam/PrintNC.GrblHAL.json create mode 100644 src2/kiri-dev/cam/RatRig.StrongHold.ONE.750x750mm.json create mode 100644 src2/kiri-dev/cam/Roland.MDX-50.json create mode 100644 src2/kiri-dev/cam/ShopBot.Basic.json create mode 100644 src2/kiri-dev/cam/Sienci.AltMill.json create mode 100644 src2/kiri-dev/cam/Sienci.LongMill.MK2.json create mode 100644 src2/kiri-dev/cam/Sienci.LongMill.json create mode 100644 src2/kiri-dev/cam/Sienci.Mill.One.json create mode 100644 src2/kiri-dev/cam/Snapmaker.Original.json create mode 100644 src2/kiri-dev/cam/Techno.Davinci.CNC.json create mode 100644 src2/kiri-dev/cam/Tormach.24R.json create mode 100644 src2/kiri-dev/cam/V1Engineering.Lowrider3.json create mode 100644 src2/kiri-dev/cam/V1Engineering.MPCNC.json create mode 100644 src2/kiri-dev/drag/Any.Generic.DragKnife.json create mode 100644 src2/kiri-dev/fdm/Any.Generic.Klipper.json create mode 100644 src2/kiri-dev/fdm/Any.Generic.Marlin.json create mode 100644 src2/kiri-dev/fdm/Anycubic.Chiron.json create mode 100644 src2/kiri-dev/fdm/Anycubic.Kobra.2.json create mode 100644 src2/kiri-dev/fdm/Anycubic.Kobra.json create mode 100644 src2/kiri-dev/fdm/Anycubic.Predator.json create mode 100644 src2/kiri-dev/fdm/Anycubic.i3.Mega.json create mode 100644 src2/kiri-dev/fdm/Atom.v2.json create mode 100644 src2/kiri-dev/fdm/Bambu.A1.json create mode 100644 src2/kiri-dev/fdm/Bambu.P1S.json create mode 100644 src2/kiri-dev/fdm/Creality.CR-10S-Pro.json create mode 100644 src2/kiri-dev/fdm/Creality.CR-10S.json create mode 100644 src2/kiri-dev/fdm/Creality.CR-30.json create mode 100644 src2/kiri-dev/fdm/Creality.Ender.3.json create mode 100644 src2/kiri-dev/fdm/Creality.Ender.5.json create mode 100644 src2/kiri-dev/fdm/Creality.Ender.6.json create mode 100644 src2/kiri-dev/fdm/Creality.K1.json create mode 100644 src2/kiri-dev/fdm/Creality.Sermoon.D1.json create mode 100644 src2/kiri-dev/fdm/Cubicon.Single+.json create mode 100644 src2/kiri-dev/fdm/Cubicon.Style.json create mode 100644 src2/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json create mode 100644 src2/kiri-dev/fdm/DeltaMaker.2T.json create mode 100644 src2/kiri-dev/fdm/DeltaMaker.Original.json create mode 100644 src2/kiri-dev/fdm/FLSUN.QQ-S.json create mode 100644 src2/kiri-dev/fdm/Flashforge.Finder.json create mode 100644 src2/kiri-dev/fdm/Flashforge.Guider.II.json create mode 100644 src2/kiri-dev/fdm/FolgerTech.FT5.json create mode 100644 src2/kiri-dev/fdm/GridBot.One.json create mode 100644 src2/kiri-dev/fdm/GridBot.Two.json create mode 100644 src2/kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json create mode 100644 src2/kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json create mode 100644 src2/kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json create mode 100644 src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json create mode 100644 src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json create mode 100644 src2/kiri-dev/fdm/Leapfrog.CreatrHS.json create mode 100644 src2/kiri-dev/fdm/MakerGear.M2.json create mode 100644 src2/kiri-dev/fdm/Makerbot.Replicator2.json create mode 100644 src2/kiri-dev/fdm/My.Bambu.P1S create mode 100644 src2/kiri-dev/fdm/Prusa.i3.MK2S.json create mode 100644 src2/kiri-dev/fdm/Prusa.i3.MK3S+.json create mode 100644 src2/kiri-dev/fdm/Prusa.mini.json create mode 100644 src2/kiri-dev/fdm/Raise3D.N2.json create mode 100644 src2/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json create mode 100644 src2/kiri-dev/fdm/SainSmart.Infi-20.json create mode 100644 src2/kiri-dev/fdm/Snapmaker.Original.json create mode 100644 src2/kiri-dev/fdm/Tina2.Basic.json create mode 100644 src2/kiri-dev/fdm/TypeA.Series1.json create mode 100644 src2/kiri-dev/fdm/TypeA.Series1Pro.json create mode 100644 src2/kiri-dev/fdm/ULIO.3D.BRIX.json create mode 100644 src2/kiri-dev/fdm/ULIO.3D.STEAM.json create mode 100644 src2/kiri-dev/fdm/Ultimaker.Ultimaker2.json create mode 100644 src2/kiri-dev/fdm/Wanhao.Duplicator_4s.json create mode 100644 src2/kiri-dev/fdm/Wanhao.Duplicator_5s.json create mode 100644 src2/kiri-dev/fdm/Wanhao.Duplicator_6.json create mode 100644 src2/kiri-dev/fdm/Wanhao.Duplicator_i3.json create mode 100644 src2/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json create mode 100644 src2/kiri-dev/fdm/XYZ.daVinci.Mini.w.json create mode 100644 src2/kiri-dev/laser/Any.Generic.Laser.json create mode 100644 src2/kiri-dev/laser/Glowforge.json create mode 100644 src2/kiri-dev/laser/Snapmaker.A250T.json create mode 100644 src2/kiri-dev/laser/Snapmaker.A350T.json create mode 100644 src2/kiri-dev/laser/Snapmaker.Original.json create mode 100644 src2/kiri-dev/laser/Xtool.P2.json create mode 100644 src2/kiri-dev/laser/xTool.D1.json create mode 100644 src2/kiri-dev/laser/xTool.F1.Ultra.json create mode 100644 src2/kiri-dev/laser/xTool.F1.json create mode 100644 src2/kiri-dev/laser/xTool.M1.json create mode 100644 src2/kiri-dev/laser/xTool.P2S.json create mode 100644 src2/kiri-dev/laser/xTool.S1.json create mode 100644 src2/kiri-dev/sla/Anycubic.Photon.S.json create mode 100644 src2/kiri-dev/sla/Anycubic.Photon.json create mode 100644 src2/kiri-dev/sla/Creality.Halot.Max.json create mode 100644 src2/kiri-dev/sla/Creality.Halot.One.json create mode 100644 src2/kiri-dev/sla/Creality.Halot.Sky.json create mode 100644 src2/kiri-dev/wedm/Any.Generic.WireEDM.json create mode 100644 src2/kiri-dev/wedm/RackRobo.Betta.Wire.V1.json create mode 100644 src2/kiri-dev/wjet/Any.Generic.WaterJet.json create mode 100644 src2/kiri-dev/wjet/HydroBLADE.json create mode 100644 src2/kiri-mode/cam/anim-2d-be.js create mode 100644 src2/kiri-mode/cam/anim-2d-fe.js create mode 100644 src2/kiri-mode/cam/anim-3d-be.js create mode 100644 src2/kiri-mode/cam/anim-3d-fe.js create mode 100644 src2/kiri-mode/cam/client.js create mode 100644 src2/kiri-mode/cam/driver-be.js create mode 100644 src2/kiri-mode/cam/driver-fe.js create mode 100644 src2/kiri-mode/cam/export.js create mode 100644 src2/kiri-mode/cam/ops.js create mode 100644 src2/kiri-mode/cam/prepare.js create mode 100644 src2/kiri-mode/cam/slice.js create mode 100644 src2/kiri-mode/cam/slicer.js create mode 100644 src2/kiri-mode/cam/slicer2.js create mode 100644 src2/kiri-mode/cam/tool.js create mode 100644 src2/kiri-mode/cam/tools.js create mode 100644 src2/kiri-mode/cam/topo3.js create mode 100644 src2/kiri-mode/cam/topo4.js create mode 100644 src2/kiri-mode/drag/driver.js create mode 100644 src2/kiri-mode/fdm/client.js create mode 100644 src2/kiri-mode/fdm/driver.js create mode 100644 src2/kiri-mode/fdm/export.js create mode 100644 src2/kiri-mode/fdm/fill.js create mode 100644 src2/kiri-mode/fdm/post.js create mode 100644 src2/kiri-mode/fdm/prepare.js create mode 100644 src2/kiri-mode/fdm/slice.js create mode 100644 src2/kiri-mode/laser/driver.js create mode 100644 src2/kiri-mode/sla/client.js create mode 100644 src2/kiri-mode/sla/driver.js create mode 100644 src2/kiri-mode/sla/export.js create mode 100644 src2/kiri-mode/sla/slice.js create mode 100644 src2/kiri-mode/sla/x_cxdlp.js create mode 100644 src2/kiri-mode/sla/x_photon.js create mode 100644 src2/kiri-mode/wedm/driver.js create mode 100644 src2/kiri-mode/wjet/driver.js create mode 100644 src2/kiri-run/cli.js create mode 100644 src2/kiri-run/engine.js create mode 100644 src2/kiri-run/frame.js create mode 100644 src2/kiri-run/minion.js create mode 100644 src2/kiri-run/worker.js create mode 100644 src2/main/kiri.js create mode 100644 web2/kiri/cxdlp.html create mode 100644 web2/kiri/engine.html create mode 100644 web2/kiri/favicon-mobile.png create mode 100644 web2/kiri/favicon.ico create mode 100644 web2/kiri/frame.html create mode 100644 web2/kiri/gsbox.png create mode 100644 web2/kiri/gyroid-test.html create mode 100644 web2/kiri/index.css create mode 100644 web2/kiri/index.html create mode 120000 web2/kiri/lang/da-dk.js create mode 100644 web2/kiri/lang/da.js create mode 100644 web2/kiri/lang/de-de.js create mode 120000 web2/kiri/lang/de.js create mode 120000 web2/kiri/lang/en-us.js create mode 100644 web2/kiri/lang/en.js create mode 100644 web2/kiri/lang/es-es.js create mode 120000 web2/kiri/lang/es.js create mode 120000 web2/kiri/lang/fr-fr.js create mode 100644 web2/kiri/lang/fr.js create mode 100644 web2/kiri/lang/pl-pl.js create mode 120000 web2/kiri/lang/pl.js create mode 120000 web2/kiri/lang/pt-pt.js create mode 100644 web2/kiri/lang/pt.js create mode 100644 web2/kiri/lang/zh.js create mode 100644 web2/kiri/logo-cube-128.png create mode 100644 web2/kiri/logo-cube-144.png create mode 100644 web2/kiri/logo-cube-256.png create mode 100644 web2/kiri/logo-cube-512.png create mode 100644 web2/kiri/logo-cube-64.png create mode 100644 web2/kiri/manifest.json create mode 100644 web2/kiri/style-alt.css create mode 100644 web2/kiri/style-atom3dp.css create mode 100644 web2/obj/box.stl create mode 100644 web2/obj/cube.stl create mode 100644 web2/obj/pocket-test.stl diff --git a/app.js b/app.js index cc36e0fb..13de4827 100644 --- a/app.js +++ b/app.js @@ -573,9 +573,13 @@ function handleVersion(req, res, next) { } else if (!debug) { let { path } = req.app; if (path === '/v2/lib/mesh/work.js') { - req.url = req.app.path = '/v2/lib/pack/work.js'; - } else if (path === '/v2/lib/main/mesh.js') { - req.url = req.app.path = '/v2/lib/pack/mesh.js'; + req.url = req.app.path = '/v2/lib/pack/mesh-work.js'; + } else if (path === '/v2/lib/main/mesh.js') { + req.url = req.app.path = '/v2/lib/pack/mesh-main.js'; + } else if (path === '/v2/lib/kiri/worker.js') { + req.url = req.app.path = '/v2/lib/pack/kiri-work.js'; + } else if (path === '/v2/lib/main/kiri.js') { + req.url = req.app.path = '/v2/lib/pack/kiri-main.js'; } next(); } else { @@ -686,7 +690,9 @@ function generateDevices() { map[deviceName] = JSON.parse(fs.readFileSync(PATH.join(root,type,device))); }); }); - synth.devices = `self.devices = ${JSON.stringify(devs)};`; + let dstr = JSON.stringify(devs); + synth.devices = `self.devices = ${dstr};`; + fs.writeFileSync(PATH.join(dir,"src2","pack","devices.js"), `export const devices = ${dstr};`); } // pack/concat code modules served under "/code/" @@ -848,10 +854,10 @@ function addCorsHeaders(req, res) { if (req.headers['access-control-request-private-network'] === 'true') { res.setHeader('Access-Control-Allow-Private-Network', 'true'); } - if (!crossOrigin) { + // if (!crossOrigin) { res.setHeader("Cross-Origin-Opener-Policy", 'same-origin'); res.setHeader("Cross-Origin-Embedder-Policy", 'require-corp'); - } + // } res.setHeader("Allow", "GET,POST,OPTIONS"); } @@ -929,7 +935,17 @@ function cookieValue(cookie,key) { } function rewriteHtmlVersion(req, res, next) { - if (["/kiri/","/mesh/","/meta/","/kiri/engine.html","/kiri/frame.html"].indexOf(req.app.path) >= 0) { + if ([ + "/v2/kiri/" + ].indexOf(req.app.path) >= 0) { + addCorsHeaders(req, res); + } else if ([ + "/kiri/", + "/kiri/engine.html", + "/kiri/frame.html", + "/mesh/", + "/meta/", + ].indexOf(req.app.path) >= 0) { addCorsHeaders(req, res); let real_write = res.write; let real_end = res.end; @@ -941,10 +957,17 @@ function rewriteHtmlVersion(req, res, next) { vstr = vstr.substring(0,mlen); } res.write = function() { - arguments[0] = arguments[0].toString().replace(/{{version}}/g,vstr); - real_write.apply(res, arguments); + try { + console.log('res.write', req.app.path, arguments[0].length); + arguments[0] = arguments[0].toString().replace(/{{version}}/g,vstr); + console.log('new length', arguments[0].length); + real_write.apply(res, arguments); + } catch (err) { + console.log({ rewrite_error: err }); + } }; res.end = function() { + console.log('res.write', req.app.path, arguments[0]?.length); if (arguments[0]) { arguments[0] = arguments[0].toString().replace(/{{version}}/g,vstr); } diff --git a/src2/cli/kiri-cam-device.json b/src2/cli/kiri-cam-device.json new file mode 100644 index 00000000..c649812b --- /dev/null +++ b/src2/cli/kiri-cam-device.json @@ -0,0 +1,26 @@ +{ + "new": false, + "mode": "CAM", + "deviceName": "My Sienci LongMill", + "noclone":false, + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 760, + "bedDepth": 760, + "maxHeight": 300, + "originCenter": false, + "spindleMax": 0, + "gcodeSpace": true, + "gcodeStrip": false, + "gcodePre": [ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)", + "G0 F6000 ; default rapid move speed", + "G1 F1500 ; default cutting speed" + ], + "gcodePost": ["M30 ; program end"], + "gcodeDwell": ["G4 P{time} ; dwell for {time}ms"], + "gcodeSpindle": [], + "gcodeChange": ["M6 T{tool} ; change tool to '{tool_name}'"], + "gcodeFExt": "nc" +} diff --git a/src2/cli/kiri-cam-process.json b/src2/cli/kiri-cam-process.json new file mode 100644 index 00000000..7055d629 --- /dev/null +++ b/src2/cli/kiri-cam-process.json @@ -0,0 +1,121 @@ +{ + "processName": "default", + "camLevelTool": 1000, + "camLevelSpindle": 1000, + "camLevelOver": 0, + "camLevelSpeed": 1000, + "camLevelDown": 1, + "camRoughTool": 1001, + "camRoughSpindle": 1000, + "camRoughDown": 3, + "camRoughOver": 0.5, + "camRoughSpeed": 1000, + "camRoughPlunge": 250, + "camRoughStock": 0, + "camRoughVoid": false, + "camRoughFlat": false, + "camRoughTop": false, + "camRoughIn": true, + "camRoughOn": true, + "camOutlineTool": 1000, + "camOutlineSpindle": 1000, + "camOutlineDown": 1, + "camOutlineOver": 0.4, + "camOutlineSpeed": 800, + "camOutlinePlunge": 250, + "camOutlineWide": false, + "camOutlineDogbone": false, + "camOutlineOmitThru": false, + "camOutlineOut": true, + "camOutlineIn": false, + "camOutlineOn": true, + "camContourTool": 1001, + "camContourSpindle": 1000, + "camContourOver": 0.25, + "camContourSpeed": 1000, + "camContourAngle": 85, + "camContourCurves": true, + "camContourIn": true, + "camContourXOn": true, + "camContourYOn": true, + "camTraceTool": 1000, + "camTraceSpindle": 1000, + "camTraceType": "clear", + "camTraceOver": 0.5, + "camTraceDown": 50, + "camTraceSpeed": 750, + "camTracePlunge": 200, + "camTraceLines": false, + "camDrillTool": 1000, + "camDrillSpindle": 1000, + "camDrillDownSpeed": 250, + "camDrillDown": 2, + "camDrillDwell": 250, + "camDrillLift": 2, + "camDrillingOn": false, + "camRegisterSpeed": 1000, + "camFlipAxis": "X", + "camFlipOther": "", + "camTabsWidth": 20, + "camTabsHeight": 5, + "camTabsDepth": 10, + "camTabsMidline": false, + "camDepthFirst": true, + "camEaseDown": false, + "camOriginTop": true, + "camZAnchor": "middle", + "camZOffset": 0, + "camZBottom": 0, + "camZClearance": 1, + "camZThru": 0, + "camFastFeed": 4000, + "camFastFeedZ": 250, + "camTolerance": 0.03, + "camStockX": 30, + "camStockY": 30, + "camStockZ": 1, + "camStockOffset": true, + "camStockClipTo": false, + "camStockOn": true, + "camConventional": false, + "ctOriginCenter": false, + "outputInvertX": false, + "outputInvertY": false, + "camExpertFast": false, + "ops": [{ + "type": "rough", + "tool": 1001, + "spindle": 1000, + "down": 3, + "step": 0.5, + "rate": 1000, + "plunge": 250, + "leave": 0, + "voids": false, + "flats": false, + "inside": true, + "top": false + }], + "camTrueShadow": false, + "camArcTolerance": 0.006, + "camArcResolution": 5, + "camDrillMark": true, + "camPocketSpindle": 1000, + "camPocketTool": 1665454124874, + "camPocketOver": 0.15, + "camPocketDown": 1, + "camPocketSpeed": 250, + "camPocketPlunge": 200, + "camPocketExpand": 0, + "camContourBottom": false, + "camTraceBottom": false, + "cmaPocketOutline": false, + "camRegisterThru": 5, + "camFlatness": 0, + "camContourBridge": 10, + "camForceZMax": false, + "camPocketSmooth": 0, + "cmaPocketRefine": 20, + "camPocketContour": false, + "camPocketEngrave": false +} diff --git a/src2/cli/kiri-cam-tools.json b/src2/cli/kiri-cam-tools.json new file mode 100644 index 00000000..234e3d1b --- /dev/null +++ b/src2/cli/kiri-cam-tools.json @@ -0,0 +1,98 @@ +[ + { + "id": 2000, + "number": 5, + "name": "ball 1/16", + "type": "ballmill", + "shaft_diam": 0.0625, + "shaft_len": 1, + "flute_diam": 0.0625, + "flute_len": 2, + "taper_tip": 0, + "metric": false + }, + { + "id": 2001, + "number": 6, + "name": "ball 1/8", + "type": "ballmill", + "shaft_diam": 0.125, + "shaft_len": 1, + "flute_diam": 0.125, + "flute_len": 2, + "taper_tip": 0, + "metric": false + }, + { + "id": 2002, + "number": 4, + "name": "ball 1/4", + "type": "ballmill", + "shaft_diam": 0.25, + "shaft_len": 1, + "flute_diam": 0.25, + "flute_len": 2, + "taper_tip": 0, + "metric": false + }, + { + "id": 1000, + "number": 3, + "type": "endmill", + "name": "end 1/16", + "metric": false, + "shaft_diam": 0.0625, + "shaft_len": 1, + "flute_diam": 0.0625, + "flute_len": 1.5, + "taper_tip": 0 + }, + { + "id": 1001, + "number": 2, + "type": "endmill", + "name": "end 1/8", + "metric": false, + "shaft_diam": 0.125, + "shaft_len": 1, + "flute_diam": 0.125, + "flute_len": 1.5, + "taper_tip": 0 + }, + { + "id": 1002, + "number": 1, + "type": "endmill", + "name": "end 1/4", + "metric": false, + "shaft_diam": 0.25, + "shaft_len": 1, + "flute_diam": 0.25, + "flute_len": 2, + "taper_tip": 0 + }, + { + "id": 1005, + "number": 6, + "type": "drill", + "name": "drill 1/8", + "metric": false, + "shaft_diam": 0.125, + "shaft_len": 1, + "flute_diam": 0.125, + "flute_len": 1.5, + "taper_tip": 0 + }, + { + "id": 1006, + "number": 7, + "type": "drill", + "name": "drill 1/4", + "metric": false, + "shaft_diam": 0.25, + "shaft_len": 1, + "flute_diam": 0.25, + "flute_len": 2, + "taper_tip": 0 + } +] \ No newline at end of file diff --git a/src2/cli/kiri-controller.json b/src2/cli/kiri-controller.json new file mode 100644 index 00000000..c5148024 --- /dev/null +++ b/src2/cli/kiri-controller.json @@ -0,0 +1,4 @@ +{ + "threaded":false, + "assembly":false +} diff --git a/src2/cli/kiri-fdm-device.json b/src2/cli/kiri-fdm-device.json new file mode 100644 index 00000000..508a0f62 --- /dev/null +++ b/src2/cli/kiri-fdm-device.json @@ -0,0 +1,62 @@ +{ + "new": false, + "mode": "FDM", + "deviceName":"Creality.Ender.3", + "noclone": false, + "internal": 0, + "imageURL": "", + "imageScale": 0.75, + "imageAnchor": 0, + "bedHeight": 2.5, + "bedWidth": 220, + "bedDepth": 220, + "bedRound": false, + "bedBelt": false, + "maxHeight": 300, + "originCenter": false, + "extrudeAbs": true, + "spindleMax": 0, + "gcodeFan": ["M106 S{fan_speed}"], + "gcodeTrack": [], + "gcodeLayer": [], + "gcodePre":[ + "M107 ; turn off filament cooling fan", + "G90 ; set absolute positioning mode", + "M82 ; set absolute positioning for extruder", + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G28 ; home axes", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 X0 Y0 Z0.25 F180 ; move XY to 0,0 and Z 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M190 S{bed_temp} T{tool} ; wait for bed to reach target temp", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "gcodePost":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G1 X0 Y300 F1200 ; end move", + "M84 ; disable stepper motors" + ], + "gcodeProc": "", + "gcodePause": [], + "gcodeDwell": [], + "gcodeSpindle": [], + "gcodeChange": [], + "gcodeFExt": "gcode", + "gcodeSpace": true, + "gcodeStrip": false, + "gcodeLaserOn": [], + "gcodeLaserOff":[], + "extruders":[ { + "extFilament":1.75, + "extNozzle":0.4, + "extSelect":[ "T0" ], + "extDeselect":[], + "extOffsetX":0, + "extOffsetY":0 + } ], +} diff --git a/src2/cli/kiri-fdm-process.json b/src2/cli/kiri-fdm-process.json new file mode 100644 index 00000000..22971357 --- /dev/null +++ b/src2/cli/kiri-fdm-process.json @@ -0,0 +1,69 @@ +{ + "sName": "Ender3_test", + "arcTolerance": 0, + "antiBacklash": 0, + "sliceHeight": 0.25, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.3, + "sliceFillSparse": 0.2, + "sliceFillType": "gyroid", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 1, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerFanSpeed": 0, + "firstLayerPrintMult": 1.15, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 3, + "outputTemp": 210, + "outputBedTemp": 60, + "outputFeedrate": 50, + "outputFinishrate": 50, + "outputSeekrate": 80, + "outputShellMult": 1.25, + "outputFillMult": 1.25, + "outputSparseMult": 1.25, + "outputRetractDist": 4, + "outputRetractSpeed": 30, + "outputRetractDwell": 30, + "outputShortPoly": 100, + "outputMinSpeed": 10, + "outputCoastDist": 0.1, + "outputLayerRetract": true, + "detectThinWalls": true, + "ctOriginCenter": false, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.25, + "firstLayerFillRate": 35, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrimTrig": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputRetractWipe": 0, + "outputBrimCount": 2, + "outputBrimOffset": 2, + "outputLoopLayers": null, + "outputInvertX": false, + "outputInvertY": false, + "outputFanSpeed": 255, + "gcodePause": "", + "ranges": [], + "zHopDistance": 0 +} diff --git a/src2/cli/kiri-source.json b/src2/cli/kiri-source.json new file mode 100644 index 00000000..2aca4448 --- /dev/null +++ b/src2/cli/kiri-source.json @@ -0,0 +1,59 @@ +[ + "main/gapp", + "moto/broker", + "moto/license", + "add/array", + "add/class", + "ext/three", + "add/three", + "ext/clip2", + "ext/earcut", + "ext/pngjs", + "geo/base", + "geo/bounds", + "geo/line", + "geo/point", + "geo/paths", + "geo/points", + "geo/gyroid", + "geo/polygons", + "geo/polygon", + "geo/slicer", + "geo/slope", + "geo/wasm", + "data/local", + "load/stl", + "kiri/conf", + "kiri/utils", + "kiri/slice", + "kiri/consts", + "kiri/client", + "kiri/widget", + "kiri/layers", + "kiri-mode/cam/driver", + "kiri-mode/cam/export", + "kiri-mode/cam/ops", + "kiri-mode/cam/prepare", + "kiri-mode/cam/slice", + "kiri-mode/cam/slicer", + "kiri-mode/cam/tool", + "kiri-mode/cam/topo", + "kiri-mode/fdm/driver", + "kiri-mode/fdm/export", + "kiri-mode/fdm/fill", + "kiri-mode/fdm/post", + "kiri-mode/fdm/prepare", + "kiri-mode/fdm/slice", + "kiri-mode/laser/driver", + "kiri-mode/sla/driver", + "kiri-mode/sla/x_cxdlp", + "kiri-mode/sla/x_photon", + "kiri-mode/sla/export", + "kiri-mode/sla/slice", + "kiri/codec", + "kiri/print", + "kiri/pack", + "kiri-run/worker", + "kiri-run/engine", + "main/kiri" +] diff --git a/src2/data/index.js b/src2/data/index.js index 521016ba..e28217be 100644 --- a/src2/data/index.js +++ b/src2/data/index.js @@ -1,20 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -// Create global data object if it doesn't exist -if (!self.data) { - self.data = {}; -} - -const { data } = self; - -data.Index = IDBStore; - -data.open = function() { - return new IDBStore(...arguments); -}; - // https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API +export const data = { + Index: IDBStore, + open() { + return new IDBStore(...arguments); + } +}; + let IDB, IRR, local = null; try { @@ -320,6 +314,5 @@ SP.clear = function(key, store = this.current) { .objectStore(store).clear(); }; -// Export for ES modules -export { IDBStore as Index }; export const open = data.open; +export const Index = IDBStore; diff --git a/src2/data/local.js b/src2/data/local.js index 7c344c2a..41116f9c 100644 --- a/src2/data/local.js +++ b/src2/data/local.js @@ -1,49 +1,41 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -// Create global data object if it doesn't exist -if (!self.data) { - self.data = {}; +class Local { + __data__ = {}; + __mem__ = true; + + getItem(key) { + return this[key]; + } + + setItem(key, val) { + this.__data__[key] = val; + this[key] = val; + } + + removeItem(key) { + delete this.__data__[key]; + } + + clear() { + this.__data__ = {}; + } } -const { data } = self; - -function Local() { - this.__data__ = {}; - this.__mem__ = true; -} - -var LS = Local.prototype; - -LS.getItem = function(key) { - return this[key]; -}; - -LS.setItem = function(key, val) { - this.__data__[key] = val; - this[key] = val; -}; - -LS.removeItem = function(key) { - delete this.__data__[key]; -}; - -LS.clear = function() { - this.__data__ = {}; -}; +let setLocal; try { // deprecate 'Local' at some point - let local = data.local = self.localStorage; + setLocal = self.localStorage; let testkey = '__test'; - local.setItem(testkey, 1); - local.getItem(testkey); - local.removeItem(testkey); + setLocal.setItem(testkey, 1); + setLocal.getItem(testkey); + setLocal.removeItem(testkey); } catch (e) { - data.local = new Local(); + setLocal = new Local(); let msg = "localStorage disabled: application may not function properly"; console.log(msg); // alert(msg); } -// Export for ES modules -export const local = data.local; +export const local = setLocal; diff --git a/src2/dock/Dockerfile b/src2/dock/Dockerfile new file mode 100644 index 00000000..d38ff39f --- /dev/null +++ b/src2/dock/Dockerfile @@ -0,0 +1,14 @@ +FROM node:18-slim +WORKDIR /grid +COPY package.json /grid +COPY app.js /grid +COPY apps /grid/apps +COPY src /grid/src +COPY web /grid/web +COPY bin /grid/bin +RUN apt-get -y update; apt-get -y install curl +EXPOSE 8080 +RUN npm i +RUN npm install -g @gridspace/app-server +RUN rm -rf /grid/bin +CMD gs-app-server diff --git a/src2/dock/compose.yml b/src2/dock/compose.yml new file mode 100644 index 00000000..208fdb45 --- /dev/null +++ b/src2/dock/compose.yml @@ -0,0 +1,10 @@ +version: '3.1' + +services: + kirimoto: + container_name: grid-apps + build: + context: ../.. + dockerfile: src/dock/Dockerfile + ports: + - "8080:8080" diff --git a/src2/geo/paths.js b/src2/geo/paths.js index 513a0f6d..b6b303d8 100644 --- a/src2/geo/paths.js +++ b/src2/geo/paths.js @@ -519,6 +519,98 @@ export function shapeToPath(shape, points, closed) { return {index, faces}; } +/** + * Generate a list of points approximating a circular arc. + * @param {Point} start - the starting point of the arc. + * @param {Point} end - the ending point of the arc. + * @param {number} [arcdivs= 24] - the number of lines to use to represent PI radians + * @param {number} opts.radius - the radius of the arc. If undefined, will use the + * start and end points to infer the radius. + * @param {boolean} opts.clockwise - whether the arc is clockwise or counter-clockwise. + * generating the points. + * + * @return {Array} an array of points representing the arc. + */ +export function arcToPath (start, end, arcdivs=24, opts) { + + let { clockwise, center, radius } = opts; + + // @type {Point} + if (end.x === undefined && end.x === undefined && center === undefined) { + // bambu generates loop z or wipe loop arcs in place + // console.log({ skip_empty_arc: rec }); + return; + } + + if (center) { + // center = center.add(start); + center.r = center.distTo2D(start); + } else if (radius !== undefined) { + let pd = { x: end.x - start.x, y: end.y - start.y }; //position delta + let dst = Math.sqrt(pd.x * pd.x + pd.y * pd.y) / 2; // distance + let pr2; + if (Math.abs(dst - radius) < 0.001) { + // center point radius + pr2 = { x: (end.x + start.x) / 2, y: (end.y + start.y) / 2}; + } else { + // triangulate + pr2 = base.util.center2pr(start, end, radius, clockwise); + } + center.x = pr2.x; + center.y = pr2.y; + center.r = radius; + } else { + console.log({malfomed_arc: {radius,center, clockwise, start, end}}); + } + + //deltas + let dx = start.x - end.x; + let dy = start.y - end.y; + let dz = start.z - end.z; + + // line angles + let a1 = Math.atan2(center.y - start.y, center.x - start.x) + Math.PI; + let a2 = Math.atan2(center.y - end.y, center.x - end.x) + Math.PI; + let ad = base.util.thetaDiff(a1, a2, clockwise); // angle difference in radians + let samePoint = Math.abs(ad) < 0.001 + let ofFull = Math.abs(ad)/(2*Math.PI); + let steps = samePoint? arcdivs : Math.max(Math.floor( arcdivs * ofFull),4); + let step = (samePoint? (Math.PI*2) : ad) / steps; + let numPoints = steps/ step; + let zStart = start.z; + let zStep = dz / numPoints; + let rot = a1 + step; + + //unused deltas + let da = Math.abs(a1 - a2); + let dd = Math.sqrt(dx * dx + dy * dy); + + // LOG({index, da, dd, first: pos, last: rec, center, a1, a2, ad, step, steps, rot, line}); + // G0G1(false, [`X${center.x}`, `Y${center.y}`, `E1`]); + + // under 1 degree arc and 5mm, convert to straight line + // if (da < 0.005 && dd < 5) { + // G0G1(false, [`X${end.x}`, `Y${end.y}`, `E1`]); + // return ; + // } + + let arr = [] // point accumulator + for (let i=0; i<=steps-2; i++) { + if (isNaN(center.r) || isNaN(center.x) || isNaN(center.y)) { + console.log({malfomed_arc: {radius, clockwise, start, end}}); + } + arr.push(newPoint( + center.x + Math.cos(rot) * center.r, + center.y + Math.sin(rot) * center.r, + zStart, + )); + zStart += zStep; + rot += step; + } + // console.log(arr,start,end); + return arr +} + export class FloatPacker { constructor(size, factor) { this.size = size; diff --git a/src2/geo/point.js b/src2/geo/point.js index bb185e42..c59a0d39 100644 --- a/src2/geo/point.js +++ b/src2/geo/point.js @@ -1,9 +1,10 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { THREE } from '../ext/three.js'; -const { Vector3 } = THREE; - import { util, config, key } from './base.js'; +import { newLine } from './line.js'; +import { newSlope } from './slope.js'; + +const { Vector3 } = THREE; export class Point { constructor(x = 0, y = 0, z = 0, key) { @@ -122,11 +123,11 @@ export class Point { } slopeTo(p) { - return base.newSlope(this, p); + return newSlope(this, p); } lineTo(p, k) { - return base.newLine(this, p, k); + return newLine(this, p, k); } isNear(p, dist) { @@ -292,7 +293,7 @@ export class Point { np2 = newPoint(p2.x - oy, p2.y + ox, p2.z, key.NONE); np1.op = p1; np2.op = p2; - return base.newLine(np1, np2, key.NONE); + return newLine(np1, np2, key.NONE); } offset(x, y, z) { diff --git a/src2/geo/points.js b/src2/geo/points.js index 0541f0f1..183306bd 100644 --- a/src2/geo/points.js +++ b/src2/geo/points.js @@ -2,10 +2,9 @@ "use strict"; +import { config } from './base.js'; import { newPoint } from '../geo/point.js'; -const { config } = base; - /** * converts a geometry point array into a kiri point array * with optional decimation diff --git a/src2/geo/polygon.js b/src2/geo/polygon.js index 3d2c0b8b..e9a424a2 100644 --- a/src2/geo/polygon.js +++ b/src2/geo/polygon.js @@ -4,9 +4,8 @@ import { base, config, util } from './base.js'; import { ClipperLib } from '../ext/clip2.esm.js'; -import { THREE } from '../ext/three.js'; import { newBounds } from './bounds.js'; -import { newPoint } from './point.js'; +import { newPoint, pointFromClipper } from './point.js'; import { polygons } from './polygons.js'; import { earcut } from '../geo/base.js'; @@ -2044,7 +2043,7 @@ export class Polygon { return res.map(array => { let poly = newPolygon(); for (let pt of array) { - poly.push(base.pointFromClipper(pt, z)); + poly.push(pointFromClipper(pt, z)); } return poly; }); @@ -2374,7 +2373,7 @@ export function fromClipperPath(path, z) { l = path.length; while (i < l) { // poly.push(newPoint(null,null,z,null,path[i++])); - poly.push(base.pointFromClipper(path[i++], z)); + poly.push(pointFromClipper(path[i++], z)); } return poly; } diff --git a/src2/geo/polygons.js b/src2/geo/polygons.js index dc172895..24f46b38 100644 --- a/src2/geo/polygons.js +++ b/src2/geo/polygons.js @@ -5,6 +5,7 @@ import { base, util, paths, config } from './base.js'; import { newPoint, pointFromClipper } from './point.js'; import { newPolygon } from './polygon.js'; +import { newSlope } from './slope.js'; import { ClipperLib } from '../ext/clip2.esm.js'; const { sqr, numOrDefault } = util; @@ -598,7 +599,7 @@ export function offset(polys, dist, opts = {}) { let open = opts.open ? polys.filter(p => p.open) : []; if (open.length) { open = open.map(p => paths.pointsToPath(p.points, dist, true)); - open = open.map(p => base.newPolygon().setOpen().addPoints(p.right)); + open = open.map(p => newPolygon().setOpen().addPoints(p.right)); } // do not use clipper to offset open lines @@ -763,7 +764,7 @@ export function fillArea(polys, angle, spacing, output, minLen, maxLen) { while (angle > 90) angle -= 180; // X,Y ray slope derived from angle - raySlope = base.newSlope(0,0, + raySlope = newSlope(0,0, Math.cos(angle * DEG2RAD) * spacing, Math.sin(angle * DEG2RAD) * spacing ); @@ -835,8 +836,8 @@ export function fillArea(polys, angle, spacing, output, minLen, maxLen) { if (minlen && plen < minlen) continue; if (maxlen && plen > maxlen) continue; } - let p1 = base.pointFromClipper(poly.m_polygon[0], zpos); - let p2 = base.pointFromClipper(poly.m_polygon[1], zpos); + let p1 = pointFromClipper(poly.m_polygon[0], zpos); + let p2 = pointFromClipper(poly.m_polygon[1], zpos); let od = rayint.origin.distToLineNew(p1,p2) / spacing; lines.push([p1, p2, od]); } diff --git a/src2/kiri-dev/cam/Any.Generic.Grbl.json b/src2/kiri-dev/cam/Any.Generic.Grbl.json new file mode 100644 index 00000000..5341c0ef --- /dev/null +++ b/src2/kiri-dev/cam/Any.Generic.Grbl.json @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 400, + "bed_depth": 400 + } +} diff --git a/src2/kiri-dev/cam/Any.Generic.LinuxCNC.json b/src2/kiri-dev/cam/Any.Generic.LinuxCNC.json new file mode 100644 index 00000000..b1406adf --- /dev/null +++ b/src2/kiri-dev/cam/Any.Generic.LinuxCNC.json @@ -0,0 +1,24 @@ +{ + "file-ext": "ngc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)", + "M03 S20000 ; spindle on" + ], + "post":[ + "M05 ; spindle off", + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 400, + "bed_depth": 400 + } +} diff --git a/src2/kiri-dev/cam/Any.Generic.TinyG.json b/src2/kiri-dev/cam/Any.Generic.TinyG.json new file mode 100644 index 00000000..5341c0ef --- /dev/null +++ b/src2/kiri-dev/cam/Any.Generic.TinyG.json @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 400, + "bed_depth": 400 + } +} diff --git a/src2/kiri-dev/cam/Carbide3D.Nomad.883Pro.json b/src2/kiri-dev/cam/Carbide3D.Nomad.883Pro.json new file mode 100644 index 00000000..fd8f0378 --- /dev/null +++ b/src2/kiri-dev/cam/Carbide3D.Nomad.883Pro.json @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 200, + "bed_depth": 200 + } +} diff --git a/src2/kiri-dev/cam/Carbide3D.Shapeoko.3.json b/src2/kiri-dev/cam/Carbide3D.Shapeoko.3.json new file mode 100644 index 00000000..439356cf --- /dev/null +++ b/src2/kiri-dev/cam/Carbide3D.Shapeoko.3.json @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 400, + "bed_depth": 400 + } +} diff --git a/src2/kiri-dev/cam/Carbide3D.Shapeoko.XL.json b/src2/kiri-dev/cam/Carbide3D.Shapeoko.XL.json new file mode 100644 index 00000000..94b5cf86 --- /dev/null +++ b/src2/kiri-dev/cam/Carbide3D.Shapeoko.XL.json @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 830, + "bed_depth": 400 + } +} diff --git a/src2/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json b/src2/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json new file mode 100644 index 00000000..5c5ef0d7 --- /dev/null +++ b/src2/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 830, + "bed_depth": 830 + } +} diff --git a/src2/kiri-dev/cam/Genmitsu.3018.json b/src2/kiri-dev/cam/Genmitsu.3018.json new file mode 100644 index 00000000..13c368a3 --- /dev/null +++ b/src2/kiri-dev/cam/Genmitsu.3018.json @@ -0,0 +1,24 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M5 ; spindle off", + "M30 ; program end" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "spindle": [ + "M3 S{speed}" + ], + "settings": { + "bed_width": 300, + "bed_depth": 180, + "spindle_max": 1000 + } +} diff --git a/src2/kiri-dev/cam/Genmitsu.PROVerXL.4030.json b/src2/kiri-dev/cam/Genmitsu.PROVerXL.4030.json new file mode 100644 index 00000000..7c3adfad --- /dev/null +++ b/src2/kiri-dev/cam/Genmitsu.PROVerXL.4030.json @@ -0,0 +1,24 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M5 ; spindle off", + "M30 ; program end" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "spindle": [ + "M3 S{speed}" + ], + "settings": { + "bed_width": 400, + "bed_depth": 300, + "spindle_max": 1000 + } +} diff --git a/src2/kiri-dev/cam/Inventables.Carvey.Any.json b/src2/kiri-dev/cam/Inventables.Carvey.Any.json new file mode 100644 index 00000000..b8ee3925 --- /dev/null +++ b/src2/kiri-dev/cam/Inventables.Carvey.Any.json @@ -0,0 +1,24 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "origin_center": false, + "spindle_max": 12000, + "bed_width": 290, + "bed_depth": 200 + } +} diff --git a/src2/kiri-dev/cam/Inventables.XCarve.1000mm.json b/src2/kiri-dev/cam/Inventables.XCarve.1000mm.json new file mode 100644 index 00000000..db059b0d --- /dev/null +++ b/src2/kiri-dev/cam/Inventables.XCarve.1000mm.json @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 1000, + "bed_depth": 1000 + } +} diff --git a/src2/kiri-dev/cam/Inventables.XCarve.500mm.json b/src2/kiri-dev/cam/Inventables.XCarve.500mm.json new file mode 100644 index 00000000..e84a19e3 --- /dev/null +++ b/src2/kiri-dev/cam/Inventables.XCarve.500mm.json @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 500, + "bed_depth": 500 + } +} diff --git a/src2/kiri-dev/cam/Langmuir.MR-1.json b/src2/kiri-dev/cam/Langmuir.MR-1.json new file mode 100644 index 00000000..f1611148 --- /dev/null +++ b/src2/kiri-dev/cam/Langmuir.MR-1.json @@ -0,0 +1,32 @@ +{ + "mode": "CAM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 500, + "bedDepth": 500, + "maxHeight": 150, + "originCenter": false, + "spindleMax": 8000, + "gcodePre": [ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)", + "M03 S20000 ; spindle on" + ], + "gcodePost": [ + "M05 ; spindle off", + "M30 ; program end" + ], + "gcodeDwell": [ + "G4 P{time} ; dwell for {time}ms" + ], + "gcodeSpindle": [], + "gcodeChange": [], + "gcodeFExt": "nc", + "gcodeSpace": true, + "gcodeStrip": true, + "new": false, + "deviceName": "Langmuir MR-1", + "imageURL": "", + "useLaser": false, + "profiles": [] +} diff --git a/src2/kiri-dev/cam/Makera.Carvera.json b/src2/kiri-dev/cam/Makera.Carvera.json new file mode 100644 index 00000000..69c91c89 --- /dev/null +++ b/src2/kiri-dev/cam/Makera.Carvera.json @@ -0,0 +1,179 @@ +{ + "mode": "CAM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 360, + "bedDepth": 240, + "originCenter": false, + "spindleMax": 20000, + "gcodePre": [ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)", + "G0 F2000 ; default rapid move speed", + "G1 F1000 ; default cutting speed" + ], + "gcodePost": [ + "M5 ; spindle off", + "G0 F4000 ; default rapid move speed", + "G1 F1000 ; default cutting speed", + "M30 ; program end" + ], + "gcodeDwell": [ + "G4 P{time} ; dwell for {time}ms" + ], + "gcodeSpindle": [ + "M3 S{spindle}" + ], + "gcodeChange": [ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "gcodeFExt": "nc", + "gcodeSpace": true, + "gcodeStrip": false, + "new": false, + "deviceName": "My Makera Carvera", + "maxHeight": 150, + "useLaser": true, + "imageURL": "", + "bedBelt": false, + "bedRound": false, + "fwRetract": false, + "profiles": [ + { + "processName": "default", + "camLevelTool": 1000, + "camLevelSpindle": 1000, + "camLevelOver": 0, + "camLevelSpeed": 1000, + "camLevelDown": 0, + "camRoughTool": 1666284436865, + "camRoughSpindle": 20000, + "camRoughDown": 3, + "camRoughOver": 0.25, + "camRoughSpeed": 1000, + "camRoughPlunge": 250, + "camRoughStock": 0, + "camRoughVoid": false, + "camRoughFlat": false, + "camRoughTop": false, + "camRoughIn": false, + "camRoughOn": true, + "camOutlineTool": 1665437735549, + "camOutlineSpindle": 0, + "camOutlineDown": 1, + "camOutlineOver": 0.4, + "camOutlineSpeed": 2000, + "camOutlinePlunge": 500, + "camOutlineWide": false, + "camOutlineDogbone": false, + "camOutlineOmitThru": false, + "camOutlineOut": false, + "camOutlineIn": false, + "camOutlineOn": true, + "camContourTool": 1666284436865, + "camContourSpindle": 20000, + "camContourOver": 0.15, + "camContourSpeed": 1500, + "camContourAngle": 85, + "camContourCurves": true, + "camContourIn": false, + "camContourXOn": true, + "camContourYOn": true, + "camTraceTool": 1002, + "camTraceSpindle": 1000, + "camTraceType": "clear", + "camTraceOver": 0.5, + "camTraceDown": 1, + "camTraceSpeed": 750, + "camTracePlunge": 200, + "camTraceLines": false, + "camDrillTool": 1000, + "camDrillSpindle": 1000, + "camDrillDownSpeed": 250, + "camDrillDown": 2, + "camDrillDwell": 250, + "camDrillLift": 2, + "camDrillingOn": false, + "camRegisterSpeed": 1000, + "camFlipAxis": "X", + "camFlipOther": "", + "camTabsWidth": 20, + "camTabsHeight": 5, + "camTabsDepth": 10, + "camTabsMidline": false, + "camDepthFirst": true, + "camEaseDown": false, + "camOriginTop": true, + "camZAnchor": "middle", + "camZOffset": 0, + "camZBottom": 0, + "camZClearance": 1, + "camZThru": 0, + "camFastFeed": 3000, + "camFastFeedZ": 500, + "camTolerance": 0, + "camStockX": 0, + "camStockY": 0, + "camStockZ": 0, + "camStockOffset": true, + "camStockClipTo": false, + "camStockOn": true, + "camConventional": false, + "camOriginCenter": false, + "outputInvertX": false, + "outputInvertY": false, + "camExpertFast": false, + "ops": [], + "op2": [ + { + "type": "flip", + "axis": "X", + "invert": true, + "disabled": false + } + ], + "camTrueShadow": false, + "camDrillMark": true, + "camPocketSpindle": 10000, + "camPocketTool": 1001, + "camPocketOver": 0.25, + "camPocketDown": 5, + "camPocketSpeed": 2000, + "camPocketPlunge": 500, + "camPocketExpand": 0, + "camContourBottom": false, + "camTraceBottom": false, + "cmaPocketOutline": false, + "camRegisterThru": 5, + "camFlatness": 0.001, + "camContourBridge": 10, + "camForceZMax": false, + "camPocketSmooth": 1, + "camPocketContour": true, + "cmaPocketRefine": 20, + "camPocketEngrave": false, + "camLaserEnable": [ + "M321" + ], + "camLaserDisable": "M322", + "camLaserOn": [ + "M3" + ], + "camLaserOff": [ + "M5" + ], + "camLaserSpeed": 100, + "camLaserPower": 1, + "camLaserAdaptive": true, + "camLaserAdaptMod": true, + "camLaserFlatten": false, + "camLaserFlatZ": 0, + "camLaserPowerMin": 1, + "camLaserPowerMax": 0.1, + "camLaserZMin": 0, + "camLaserZMax": 0, + "camOutlineTop": false, + "X": "X" + } + ] +} \ No newline at end of file diff --git a/src2/kiri-dev/cam/PrintNC.GrblHAL.json b/src2/kiri-dev/cam/PrintNC.GrblHAL.json new file mode 100644 index 00000000..caba6306 --- /dev/null +++ b/src2/kiri-dev/cam/PrintNC.GrblHAL.json @@ -0,0 +1,30 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M5 ; spindle off", + "M9 ; coolant off", + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "spindle":[ + "M3 S{speed}" + ], + "settings": { + "origin_center":false, + "bed_width": 950, + "bed_depth": 600, + "build_height": 180, + "spindle_max": 24000 + } +} diff --git a/src2/kiri-dev/cam/RatRig.StrongHold.ONE.750x750mm.json b/src2/kiri-dev/cam/RatRig.StrongHold.ONE.750x750mm.json new file mode 100644 index 00000000..db7bf4da --- /dev/null +++ b/src2/kiri-dev/cam/RatRig.StrongHold.ONE.750x750mm.json @@ -0,0 +1,30 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": false, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)", + "G94 ; set unit to mm/minute", + "G0 F3000 ; default rapid move speed", + "; M3 S15000 ; Uncomment if you want to turn the spindle on by default" + ], + "post":[ + "M5 ; spindle off", + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "spindle":[ + "M3 S{speed}" + ], + "settings": { + "bed_width": 744, + "bed_depth": 750, + "build_height": 90 + } +} diff --git a/src2/kiri-dev/cam/Roland.MDX-50.json b/src2/kiri-dev/cam/Roland.MDX-50.json new file mode 100644 index 00000000..8c6a283d --- /dev/null +++ b/src2/kiri-dev/cam/Roland.MDX-50.json @@ -0,0 +1,22 @@ +{ + "file-ext": "rml", + "token-space": "", + "strip-comments": true, + "pre":[ + "PA;PA;VS4;!VZ4;!PZ0,80;!MC1;" + ], + "post":[ + "PA;PA;!PZ0,2420;PU0,6096;!MC0;" + ], + "dwell":[ + "W{time};" + ], + "spindle": [ + "!RC{speed};" + ], + "settings": { + "bed_width": 400, + "bed_depth": 305, + "spindle_max": 10000 + } +} diff --git a/src2/kiri-dev/cam/ShopBot.Basic.json b/src2/kiri-dev/cam/ShopBot.Basic.json new file mode 100644 index 00000000..2480e745 --- /dev/null +++ b/src2/kiri-dev/cam/ShopBot.Basic.json @@ -0,0 +1,45 @@ +{ + "mode": "CAM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 2400, + "bedDepth": 1200, + "maxHeight": 150, + "originCenter": false, + "spindleMax": 0, + "gcodePre": [ + "'UNITS:MM", + "'", + "CN, 90 'Set program to absolute coordinate mode", + "&Tool =1 'Tool number to change to", + "C9 'Change tool", + "TR,4000 'Set spindle RPM", + "C6 'Spindle on", + "PAUSE 2", + "'" + ], + "gcodePost": [ + "'Turning router OFF", + "C7", + "END", + "'", + "UNIT_ERROR:", + "CN, 91 'Run file explaining unit error", + "END" + ], + "gcodeDwell": [ + "G4 P{time} ; dwell for {time}ms" + ], + "gcodeSpindle": [], + "gcodeChange": [ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "gcodeFExt": ".sbp", + "gcodeSpace": true, + "gcodeStrip": true, + "new": false, + "deviceName": "ShopBot.Basic", + "imageURL": "", + "useLaser": false, + "profiles": [] +} diff --git a/src2/kiri-dev/cam/Sienci.AltMill.json b/src2/kiri-dev/cam/Sienci.AltMill.json new file mode 100644 index 00000000..03f228d4 --- /dev/null +++ b/src2/kiri-dev/cam/Sienci.AltMill.json @@ -0,0 +1,30 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": false, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)", + "G0 F15000 ; default rapid move speed", + "G1 F4000 ; default cutting speed" + ], + "post":[ + "M5 ; spindle off", + "M9 ; coolant off", + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "spindle":[ + "M3 S{speed}" + ], + "settings": { + "bed_width": 1260, + "bed_depth": 1248, + "build_height": 170 + } +} diff --git a/src2/kiri-dev/cam/Sienci.LongMill.MK2.json b/src2/kiri-dev/cam/Sienci.LongMill.MK2.json new file mode 100644 index 00000000..a9e2b11d --- /dev/null +++ b/src2/kiri-dev/cam/Sienci.LongMill.MK2.json @@ -0,0 +1,27 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": false, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)", + "G0 F5500 ; default rapid move speed", + "G1 F2000 ; default cutting speed" + ], + "post":[ + "M5 ; spindle off", + "M9 ; coolant off", + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 810, + "bed_depth": 855, + "build_height": 120 + } +} diff --git a/src2/kiri-dev/cam/Sienci.LongMill.json b/src2/kiri-dev/cam/Sienci.LongMill.json new file mode 100644 index 00000000..6e4d0fff --- /dev/null +++ b/src2/kiri-dev/cam/Sienci.LongMill.json @@ -0,0 +1,30 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": false, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)", + "G0 F4000 ; default rapid move speed", + "G1 F1500 ; default cutting speed" + ], + "post":[ + "M5 ; spindle off", + "M9 ; coolant off", + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "image_url": "/decal/longmill.png", + "image_scale": 200, + "image_anchor": 8, + "bed_width": 770, + "bed_depth": 820, + "build_height": 110 + } +} diff --git a/src2/kiri-dev/cam/Sienci.Mill.One.json b/src2/kiri-dev/cam/Sienci.Mill.One.json new file mode 100644 index 00000000..69bb12dc --- /dev/null +++ b/src2/kiri-dev/cam/Sienci.Mill.One.json @@ -0,0 +1,27 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": false, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)", + "G0 F1800 ; default rapid move speed", + "G1 F800 ; default cutting speed" + ], + "post":[ + "M5 ; spindle off", + "M9 ; coolant off", + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 235, + "bed_depth": 185, + "build_height": 100 + } +} diff --git a/src2/kiri-dev/cam/Snapmaker.Original.json b/src2/kiri-dev/cam/Snapmaker.Original.json new file mode 100644 index 00000000..504b2d31 --- /dev/null +++ b/src2/kiri-dev/cam/Snapmaker.Original.json @@ -0,0 +1,27 @@ +{ + "file-ext": "gcode", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M5 ; complete moves, stop spindle", + "M30 ; program end" + ], + "tool-change":[], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "spindle":[ + "M3 P{spindle} ; uniquely uses percentage not RPM" + ], + "settings": { + "origin_center": false, + "bed_width": 90, + "bed_depth": 90, + "build_height": 50, + "spindle_max": 100 + } +} diff --git a/src2/kiri-dev/cam/Techno.Davinci.CNC.json b/src2/kiri-dev/cam/Techno.Davinci.CNC.json new file mode 100644 index 00000000..fad5eb4d --- /dev/null +++ b/src2/kiri-dev/cam/Techno.Davinci.CNC.json @@ -0,0 +1,22 @@ +{ + "file-ext": "ncd", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 250, + "bed_depth": 300 + } +} diff --git a/src2/kiri-dev/cam/Tormach.24R.json b/src2/kiri-dev/cam/Tormach.24R.json new file mode 100644 index 00000000..91b833e1 --- /dev/null +++ b/src2/kiri-dev/cam/Tormach.24R.json @@ -0,0 +1,32 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": false, + "pre":[ + "G20 ; set units to inches (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M05 ; spindle off", + "M30 ; program end" + ], + "tool-change":[ + "M05 ; spindle off", + "M6 T{tool} ; change tool to '{tool_name}'", + "G37; get tool offset with ETS" + ], + "spindle": [ + "M3 S{speed} ; spindle on at {spindle} rpm" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "origin_center":false, + "bed_width": 678.18, + "bed_depth": 1524, + "build_height":150, + "spindle_max": 24000 + }, + "deviceName": "Tormach 24R with ETS" +} \ No newline at end of file diff --git a/src2/kiri-dev/cam/V1Engineering.Lowrider3.json b/src2/kiri-dev/cam/V1Engineering.Lowrider3.json new file mode 100644 index 00000000..e237d175 --- /dev/null +++ b/src2/kiri-dev/cam/V1Engineering.Lowrider3.json @@ -0,0 +1,24 @@ +{ + "file-ext":"nc", + "token-space":" ", + "strip-comments":true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings":{ + "origin_center":false, + "bed_width":1220, + "bed_depth":2400, + "build_height":80 + } +} diff --git a/src2/kiri-dev/cam/V1Engineering.MPCNC.json b/src2/kiri-dev/cam/V1Engineering.MPCNC.json new file mode 100644 index 00000000..d20362bd --- /dev/null +++ b/src2/kiri-dev/cam/V1Engineering.MPCNC.json @@ -0,0 +1,24 @@ +{ + "file-ext":"nc", + "token-space":" ", + "strip-comments":true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings":{ + "origin_center":false, + "bed_width":450, + "bed_depth":330, + "build_height":81 + } +} diff --git a/src2/kiri-dev/drag/Any.Generic.DragKnife.json b/src2/kiri-dev/drag/Any.Generic.DragKnife.json new file mode 100644 index 00000000..5ede1faa --- /dev/null +++ b/src2/kiri-dev/drag/Any.Generic.DragKnife.json @@ -0,0 +1,21 @@ +{ + "file-ext": "gcode", + "token-space": " ", + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "laser-on":[ + "M106 S{power}" + ], + "laser-off":[ + "M107" + ], + "settings": { + "bed_width": 300, + "bed_depth": 300 + } +} diff --git a/src2/kiri-dev/fdm/Any.Generic.Klipper.json b/src2/kiri-dev/fdm/Any.Generic.Klipper.json new file mode 100644 index 00000000..d4637024 --- /dev/null +++ b/src2/kiri-dev/fdm/Any.Generic.Klipper.json @@ -0,0 +1,46 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 ; home axes", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 X0 Y0 Z0.25 F180 ; move xy to 0,0 and z 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M190 S{bed_temp} T{tool} ; wait for bed to reach target temp", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G1 E15 F200 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G1 Z{z_max} F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + },{ + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 30, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": false + } +} diff --git a/src2/kiri-dev/fdm/Any.Generic.Marlin.json b/src2/kiri-dev/fdm/Any.Generic.Marlin.json new file mode 100644 index 00000000..d4637024 --- /dev/null +++ b/src2/kiri-dev/fdm/Any.Generic.Marlin.json @@ -0,0 +1,46 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 ; home axes", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 X0 Y0 Z0.25 F180 ; move xy to 0,0 and z 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M190 S{bed_temp} T{tool} ; wait for bed to reach target temp", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G1 E15 F200 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G1 Z{z_max} F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + },{ + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 30, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": false + } +} diff --git a/src2/kiri-dev/fdm/Anycubic.Chiron.json b/src2/kiri-dev/fdm/Anycubic.Chiron.json new file mode 100644 index 00000000..0fd797ae --- /dev/null +++ b/src2/kiri-dev/fdm/Anycubic.Chiron.json @@ -0,0 +1,55 @@ +{ + "pre":[ + "M107 ; turn off fan", + "M140 S{bed_temp} T{tool}", + "M105 ; update temperature", + "M190 S{bed_temp} T{tool}", + "M104 S{temp} T{tool}", + "M105 ; update temperature", + "M109 S{temp} T{tool}", + "G21 ; set metric values", + "G91 ; relative positioning", + "G1 F1000 Z5 ; lift 5mm before homing", + "G90 ; set absolute positioning", + "M82 ; set extruder to absolute mode", + "G28 X0 Y0 ; home X & Y", + "G28 Z0 ; home Z", + "G1 Z15 F1000 ; move the platform down 15mm", + "G1 F4000 ; set default feed rate", + "G92 E0 ; zero extruder", + ";G5 ; uncomment to enable resume after power failure", + "M117 Printing..." + ], + "post":[ + "M107 ; turn off fan", + "M104 S0 T{tool} ; turn off extruder", + "M140 S0 T{tool} ; turn off bed", + "G92 E0 ; zero extruder", + "G91 ; relative positioning", + "G1 E-2 F300 ; retract filament", + "G1 Z0.5 E-5 ; retract filament, move z up", + "G90 ; absolute positioning", + "G1 X0 Y400 ; move to best position for part removal", + "M82 ; absolute extrusion mode", + "M84 ; disable stepper motors" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_circle": false, + "bed_width": 400, + "bed_depth": 400, + "build_height": 450 + } +} diff --git a/src2/kiri-dev/fdm/Anycubic.Kobra.2.json b/src2/kiri-dev/fdm/Anycubic.Kobra.2.json new file mode 100644 index 00000000..85a4ffbc --- /dev/null +++ b/src2/kiri-dev/fdm/Anycubic.Kobra.2.json @@ -0,0 +1,184 @@ +{ + "mode": "FDM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 200, + "bedDepth": 200, + "bedRound": false, + "bedBelt": false, + "maxHeight": 250, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodeLayer": [], + "gcodePre": [ + "M201 X2500 Y2500 Z800 E2500 ; sets maximum accelerations, mm/sec^2", + "M203 X250 Y250 Z8 E80 ; sets maximum feedrates, mm / sec", + "M204 P2500 R2500 T2500 ; sets acceleration (P, T) and retract acceleration (R), mm/sec^2", + "M205 X8.00 Y8.00 Z0.40 E8.00 ; sets the jerk limits, mm/sec", + "M205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec", + "M107 ; fan off", + "M190 S{bed_temp} ; set bed temperature and wait for it to be reached", + "M104 S{temp} ; set temperature", + "M109 S{temp} ; set temperature and wait for it to be reached", + "G21 ; set units to millimeters", + "G90 ; use absolute coordinates", + "M82 ; use absolute distances for extrusion", + "G92 E0", + "G28 ; home all axes\\nG1 Z5 F5000 ; lift nozzle\\n", + "G1 Z0.24 ; lift nozzle a bit ", + "G92 E0 ; zero the extruded length ", + "G1 X5 Y0 F3000", + "G1 X50 Y0 E60 F500 ; Extrude 25mm of filament in a 5cm line. ", + "G92 E0 ; zero the extruded length again ", + "G1 E-4.5 F4800 ; Retract a little ", + "G92 E0", + "G1 X120 F4000 ; Quickly wipe away from the filament line`" + ], + "gcodePost": [ + "M104 S0 ; Extruder off ", + "M140 S0 ; Heatbed off ", + "M107 ; Fan off ", + "G91 ; relative positioning ", + "G1 E-2 F3000 ", + "G1 Z+0.3 F3000 ; lift print head ", + "G28 X0 F3000", + "M84 ; disable stepper motors", + "M117 Cooling down...", + "M104 S0 ; turn off extruder", + "M107 ; Fan off", + "M84 ; disable motors", + "G91 ; relative positioning", + "G1 E-1 F300 ; retract the filament a bit before lifting the nozzle, to release some of the pressure", + "G1 Z+0.5 E-5 ;X-20 Y-20 F240 ; move Z up a bit and retract filament even more", + "G28 X0 ; move X to min endstops, so the head is out of the way", + "G90 ; Absolute positionning", + "G1 Y200 F3000 ; Present print", + "M84 ; steppers off", + "M300 P300 S4000", + "M117 Finished." + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "deviceName": "Anycubic Kobra 2", + "fwRetract": false, + "extras": { + "palette": { + "printer": "6c99ea036da", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + }, + "new": false, + "imageURL": "", + "filamentSource": "direct", + "deviceZMax": 0, + "gcodeTime": 1, + "gcodeFeature": [], + "profiles": [ + { + "processName": "default", + "sliceHeight": 0.25, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceLineWidth": 0, + "sliceFillGrow": 0, + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.25, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSolidRate": 0, + "sliceFillType": "hex", + "sliceSupportDensity": 0.1, + "sliceSupportOffset": 1, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.1, + "sliceSupportSpan": 5, + "sliceSupportGrow": 0, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstSliceHeight": 0.25, + "firstLayerRate": 30, + "firstLayerFillRate": 35, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1, + "firstLayerYOffset": 0, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFanSpeed": 0, + "firstLayerFlatten": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputDraftShield": false, + "outputTemp": 200, + "outputBedTemp": 60, + "outputFeedrate": 50, + "outputFinishrate": 50, + "outputSeekrate": 80, + "outputShellMult": 1.25, + "outputFillMult": 1.25, + "outputSparseMult": 1.25, + "outputFanLayer": 1, + "outputFanSpeed": 255, + "outputRetractDist": 1.5, + "outputRetractSpeed": 40, + "outputRetractWipe": 0, + "outputRetractDwell": 20, + "outputBrimCount": 2, + "outputBrimOffset": 2, + "outputShortPoly": 100, + "outputMinSpeed": 10, + "outputCoastDist": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputAvoidGaps": true, + "outputAlternating": false, + "outputLayerRetract": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "sliceDetectThin": "off", + "sliceMinHeight": 0, + "sliceAdaptive": false, + "zHopDistance": 0.2, + "arcTolerance": 0, + "antiBacklash": 1, + "ranges": [] + } + ] +} \ No newline at end of file diff --git a/src2/kiri-dev/fdm/Anycubic.Kobra.json b/src2/kiri-dev/fdm/Anycubic.Kobra.json new file mode 100644 index 00000000..a979015a --- /dev/null +++ b/src2/kiri-dev/fdm/Anycubic.Kobra.json @@ -0,0 +1,79 @@ +{ + "noclone": false, + "mode": "FDM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 222, + "bedDepth": 222, + "bedRound": false, + "bedBelt": false, + "resolutionX": 1600, + "resolutionY": 900, + "maxHeight": 222, + "originCenter": false, + "extrudeAbs": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodeLayer": [], + "gcodePre": [ + "M107 ; turn off filament cooling fan", + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "G28 ; home axes", + "G1 F3000 ; set default move speed", + "G1 Z1 F6000 ; pause above the bed", + "M190 S{bed_temp} T0 ; wait for bed to reach target temp", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G92 E0 ; set extruder position to 0" + ], + "gcodePost": [ + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off extruder", + "M140 S0 T0 ; turn off bed", + "G91 ; relative movement", + "G1 Z10 F1200 ; drop bed 10mm", + "G90 ; absolute positioning", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "gcodeExt": [], + "gcodeInt": [], + "gcodeProc": "", + "gcodePause": [], + "gcodeDwell": [], + "gcodeSpindle": [], + "gcodeChange": [], + "gcodeFExt": "gcode", + "gcodeSpace": true, + "gcodeStrip": false, + "gcodeLaserOn": [], + "gcodeLaserOff": [], + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "deviceName": "Anycubic Kobra", + "fwRetract": false, + "extras": { + "palette": { + "printer": "6c99ea036da", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + } +} diff --git a/src2/kiri-dev/fdm/Anycubic.Predator.json b/src2/kiri-dev/fdm/Anycubic.Predator.json new file mode 100644 index 00000000..fbd0b649 --- /dev/null +++ b/src2/kiri-dev/fdm/Anycubic.Predator.json @@ -0,0 +1,56 @@ +{ + "pre":[ + "M140 S{bed_temp} T{tool}", + "M105 ; update temperature", + "M190 S{bed_temp} T{tool}", + "M104 S{temp} T{tool}", + "M105 ; update temperature", + "M109 S{temp} T{tool}", + "M82 ; set extruder to absolute mode", + "G21 ; set metric values", + "G90 ; set absolute positioning", + "M82 ; set extruder to absolute mode", + "M107 ; turn off fan", + "G28 ; home", + "G1 Z15 F1000 ; move the platform down 15mm", + "G92 E0 ; zero extruder", + "G1 F200 E3 ; extrude 3mm of feed stock", + "G92 E0 ; zero extruder", + "G1 F4000 ; set default feed rate", + "G92 E0 ; zero extruder", + ";G5 ; uncomment to enable resume after power failure", + "M117 Printing..." + ], + "post":[ + "M107 ; turn off fan", + "M104 S0 T{tool} ; turn off extruder", + "M140 S0 T{tool} ; turn off bed", + "G91 ; relative positioning", + "G92 E0 ; zero extruder", + "G1 E-2 F300 ; retract filament", + "G1 Z0.5 E-5 ; retract filament, move z up", + "G28 ; home all axes", + "G90 ; absolute positioning", + "M82 ; absolute extrusion mode", + "M84 ; disable stepper motors" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": true, + "extrude_abs": true, + "bed_circle": true, + "bed_width": 370, + "bed_depth": 370, + "build_height": 455 + } +} diff --git a/src2/kiri-dev/fdm/Anycubic.i3.Mega.json b/src2/kiri-dev/fdm/Anycubic.i3.Mega.json new file mode 100644 index 00000000..fae91113 --- /dev/null +++ b/src2/kiri-dev/fdm/Anycubic.i3.Mega.json @@ -0,0 +1,51 @@ +{ + "pre":[ + "M105 ; update temperature", + "M104 S{temp} T{tool}", + "M140 S{bed_temp} T{tool}", + "M109 S{temp} T{tool}", + "M190 S{bed_temp} T{tool}", + "M105 ; update temperature", + "G21 ; set metric values", + "G90 ; set absolute positioning", + "M82 ; set extruder to absolute mode", + "M107 ; turn off fan", + "G28 X0 Y0 ; move X/Y to min", + "G28 Z0 ; move Z to min", + "G1 Z15.0 F100 ; move the platform down 15mm", + "G92 E0 ; zero extruder", + "G1 F200 E3 ; extrude 3mm of feed stock", + "G92 E0 ; zero extruder", + "G1 F100 ; set default feed rate", + "G0 Y20 ; wipe move to Y20", + "G92 E0 ; zero extruder", + ";G5 ; uncomment to enable resume after power failure", + "M117 Printing..." + ], + "post":[ + "M107 ; turn off fan", + "M104 S0 T{tool} ; turn off extruder", + "M140 S0 T{tool} ; turn off bed", + "G1 Z{z_max} F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 210, + "bed_depth": 210, + "build_height": 205 + } +} diff --git a/src2/kiri-dev/fdm/Atom.v2.json b/src2/kiri-dev/fdm/Atom.v2.json new file mode 100644 index 00000000..dd16941f --- /dev/null +++ b/src2/kiri-dev/fdm/Atom.v2.json @@ -0,0 +1,27 @@ +{ + "pre":[ + "M104 S{temp} ; set extruder temperature", + "M140 S{bed_temp} ; set bed temperature", + "G28 ; Home", + "G1 F5000 Z50 ; lift nozzle", + "G0 X0 Y-100 Z50 ; parking", + "G0 Z20 ; parking", + "G92 E0 ; zero the extruded length", + "G1 F200 E3 ; extrude 3mm of feed stock" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 ; turn off right extruder", + "M140 S0 ; turn off bed" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": true, + "bed_width": 210, + "bed_depth": 210, + "bed_circle": true, + "build_height": 320 + } +} diff --git a/src2/kiri-dev/fdm/Bambu.A1.json b/src2/kiri-dev/fdm/Bambu.A1.json new file mode 100644 index 00000000..401bbb71 --- /dev/null +++ b/src2/kiri-dev/fdm/Bambu.A1.json @@ -0,0 +1,816 @@ +{ + "mode": "FDM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 256, + "bedDepth": 256, + "bedRound": false, + "maxHeight": 256, + "originCenter": false, + "gcodeFan": [ + "M106 P1 S{fan_speed}" + ], + "gcodeTrack": [ + "M73 P{progress}" + ], + "gcodeLayer": [ + "; ===== CHANGE_LAYER ==============================", + "; layer num/total_layer_count: {layer}/{layers}", + "M622.1 S1 ; for prev firware, default turned on", + "M1002 judge_flag timelapse_record_flag", + "M622 J1", + "M971 S11 C10 O0 ; timelapse without wipe tower", + "M623", + "M73 L{layer}", + "M991 S0 P{layer-1} ; notify layer change", + ";; IF { layer == 0 }", + "M204 S500 ; reduce first layer acceleration", + ";; ELSE", + "M204 S5000 ; faster acceleration", + ";; END" + ], + "gcodePre": [ + "; total layer number: {layers}", + "; model label id: {model_labels}", + ";; PREAMBLE", + ";; DEFINE BAMBU-AMS 0,1,2,3", + "; ===== HEADER ====================================", + "M73 P0 R24 ; Initial progress and time remaining", + "M201 X12000 Y12000 Z1500 E5000 ; Set max acceleration", + "M203 X500 Y500 Z30 E30 ; Set max feed rates", + "M204 P12000 R5000 T12000 ; Set acceleration", + "M205 X9.00 Y9.00 Z3.00 E2.50 ; Set advanced settings", + "M106 S0 ; Set part fan speed to 0", + "M106 P2 S0 ; Turn off aux fan", + ";===== Machine: A1 ================================", + "G392 S0 ; Disable clog detection", + "M9833.2 ; Custom command (unknown function)", + ";===== Start Heating ==============================", + "M1002 gcode_claim_action : 2", + "M1002 set_filament_type:PLA ; Set filament type to PLA", + "M104 S140 ; Set nozzle temperature", + "M140 S65 ; Set bed temperature", + ";===== Startup Sound ==============================", + "M17 ; Enable steppers", + "M400 S1 ; Wait for movements to complete", + "M1006 S1 ; Start printer sound", + "M1006 A0 B10 L100 C37 D10 M60 E37 F10 N60", + "M1006 A0 B10 L100 C41 D10 M60 E41 F10 N60", + "M1006 A0 B10 L100 C44 D10 M60 E44 F10 N60", + "M1006 A0 B10 L100 C0 D10 M60 E0 F10 N60", + "M1006 A43 B10 L100 C46 D10 M70 E39 F10 N80", + "M1006 A0 B10 L100 C0 D10 M60 E0 F10 N80", + "M1006 A0 B10 L100 C43 D10 M60 E39 F10 N80", + "M1006 A0 B10 L100 C0 D10 M60 E0 F10 N80", + "M1006 A0 B10 L100 C41 D10 M80 E41 F10 N80", + "M1006 A0 B10 L100 C44 D10 M80 E44 F10 N80", + "M1006 A0 B10 L100 C49 D10 M80 E49 F10 N80", + "M1006 A0 B10 L100 C0 D10 M80 E0 F10 N80", + "M1006 A44 B10 L100 C48 D10 M60 E39 F10 N80", + "M1006 A0 B10 L100 C0 D10 M60 E0 F10 N80", + "M1006 A0 B10 L100 C44 D10 M80 E39 F10 N80", + "M1006 A0 B10 L100 C0 D10 M60 E0 F10 N80", + "M1006 A43 B10 L100 C46 D10 M60 E39 F10 N80", + "M1006 W ; Play audio", + "M18 ; Disable steppers", + ";===== Avoid End Stop =============================", + "G91 ; Set relative positioning", + "G380 S2 Z40 F1200 ; Move Z up to 40mm", + "G380 S3 Z-15 F1200 ; Move Z down to -15mm", + "G90 ; Set absolute positioning", + ";===== Reset Machine Status =======================", + "M204 S6000 ; Set acceleration to 6000 mm/s^2", + "M630 S0 P0 ; Custom command (unknown function)", + "G91 ; Set relative positioning", + "M17 Z0.3 ; Lower Z motor current", + "G90 ; Set absolute positioning", + "M17 X0.65 Y1.2 Z0.6 ; Reset motor currents", + "M960 S5 P1 ; Turn on logo lamp", + "G90 ; Set absolute positioning", + "M220 S100 ; Reset feedrate", + "M221 S100 ; Reset flowrate", + "M73.2 R1.0 ; Reset estimated time remaining", + ";===== Enable Noise Reduction =====================", + "M982.2 S1 ; Turn on noise reduction", + ";===== Home XY & Prepare Print ====================", + "M1002 gcode_claim_action : 13", + "G28 X ; Home X-axis", + "G91 ; Set relative positioning", + "G1 Z5 F1200 ; Move Z up by 5mm", + "G90 ; Set absolute positioning", + "G0 X128 F30000 ; Move X to 128mm", + "G0 Y254 F3000 ; Move Y to 254mm", + "G91 ; Set relative positioning", + "G1 Z-5 F1200 ; Move Z down by 5mm", + "M109 S25 H140 ; Set tool preheat temperature", + "M17 E0.3 ; Set extruder motor current", + "M83 ; Set extruder to relative mode", + "G1 E10 F1200 ; Extrude 10mm", + "G1 E-0.5 F30 ; Retract 0.5mm", + "M17 D ; Restore motor current", + ";===== Home Z with Low Precision ==================", + "G28 Z P0 T140 ; Home Z with low precision", + "M104 S220 ; Set nozzle temperature", + ";===== Build Plate Detection ======================", + "M1002 judge_flag build_plate_detect_flag", + "M622 S1", + " G39.4 ; Custom build plate detection command", + " G90 ; Set absolute positioning", + " G1 Z5 F1200 ; Move Z up by 5mm", + "M623", + ";===== Prepare Print Temperature & Material =======", + "M1002 gcode_claim_action : 24", + "M400 ; Wait for all movements to complete", + "M211 X0 Y0 Z0 ; Turn off soft endstops", + "M975 S1 ; Enable tool vibration suppression", + "G90 ; Set absolute positioning", + "G1 X-28.5 F30000 ; Move X-axis", + "G1 X-48.2 F3000 ; Move further on X", + "M620 M ; Enable material remap", + "M620 S0A ; Switch material if AMS exists", + " M1002 gcode_claim_action : 4", + " M400 ; Wait for heating", + " M1002 set_filament_type:UNKNOWN ; Set filament type as unknown", + " M109 S220 ; Set and wait hotend temperature", + " M104 S250 ; Set nozzle purge temp", + " M400 ; Wait for heating", + " T0 ; Select Tool 0", + " G1 X-48.2 F3000 ; Move X again", + " M400 ; Wait for completion", + " M620.1 E F523.843 T240 ; Extrude for purge sequence", + " M109 S250 ; Wait for nozzle flush temp", + " M106 P1 S0 ; Turn off nozzle cooling fan", + " G92 E0 ; Reset extruder position", + " G1 E50 F200 ; Extrude 50mm to clear nozzle", + " M400 ; Wait for all movements to complete", + " M1002 set_filament_type:PLA ; Confirm filament type", + "M621 S0A ; Confirm material switch", + "M109 S240 H300 ; Set nozzle temp", + "G92 E0 ; Reset extruder position", + "G1 E50 F200 ; Extrude 50mm slowly to prevent clog", + "M400 ; Wait for completion", + "M106 P1 S178 ; Set nozzle cooling fan speed", + "G92 E0 ; Reset extruder position", + "G1 E5 F200 ; Extrude 5mm slowly", + "M104 S220 ; Lower nozzle temperature", + "G92 E0 ; Reset extruder position", + "G1 E-0.5 F300 ; Retract filament slightly", + ";===== Wipe and Shake Nozzle ======================", + "G1 X-28.5 F30000 ; Move X-axis", + "M73 P1 R23 ; Update print progress", + "G1 X-48.2 F3000 ; Move back on X-axis", + "M73 P2 R23 ; Update print progress", + "G1 X-28.5 F30000 ; Wipe nozzle", + "G1 X-48.2 F3000 ; Move back on X", + "G1 X-28.5 F30000 ; Wipe nozzle", + "G1 X-48.2 F3000 ; Move back on X", + "M400 ; Wait for all movements to complete", + "M106 P1 S0 ; Turn off nozzle cooling fan", + ";===== Auto Extrude Calibration Start =============", + "M975 S1 ; Enable extruder calibration", + "G90 ; Set absolute positioning", + "M83 ; Set extruder to relative mode", + "T1000 ; Select tool 1000", + "G1 X-48.2 Y0 Z10 F10000 ; Move to calibration start position", + "M400 ; Wait for all moves to complete", + "M1002 set_filament_type:UNKNOWN ; Set filament type to unknown", + "M412 S1 ; Enable filament runout detection", + "M400 P10 ; Wait for 10ms", + "M620.3 W1 ; Enable filament tangle detection", + "M400 S2 ; Wait for 2ms", + "M1002 set_filament_type:PLA ; Set filament type to PLA", + "M1002 judge_flag extrude_cali_flag ; Check extruder calibration flag", + "M622 J1", + " M1002 gcode_claim_action : 8", + " M109 S220 ; Set hotend temperature", + " G1 E10 F377.08 ; Extrude 10mm at 377.08 mm/min", + " M983 F6.28466 A0.3 H0.4 ; Perform dynamic extrusion compensation", + " M106 P1 S255 ; Set nozzle cooling fan to max speed", + " M400 S5 ; Wait for 5 seconds", + " G1 X-28.5 F18000 ; Move X for wipe", + " G1 X-48.2 F3000 ; Move X back", + " G1 X-28.5 F18000 ; Wipe motion", + " G1 X-48.2 F3000 ; Move X back", + " M73 P3 R23 ; Update progress", + " G1 X-28.5 F12000 ; Wipe and shake", + " G1 X-48.2 F3000 ; Move X back", + " M400 ; Wait for all movements to complete", + " M106 P1 S0 ; Turn off nozzle cooling fan", + " M1002 judge_last_extrude_cali_success", + " M622 J0", + " M983 F6.28466 A0.3 H0.4 ; Perform dynamic extrusion compensation", + " M106 P1 S255 ; Set nozzle cooling fan to max speed", + " M400 S5 ; Wait for 5 seconds", + " G1 X-28.5 F18000 ; Move X for wipe", + " G1 X-48.2 F3000 ; Move X back", + " G1 X-28.5 F18000 ; Wipe motion", + " G1 X-48.2 F3000 ; Move X back", + " G1 X-28.5 F12000 ; Wipe and shake", + " M400 ; Wait for all movements to complete", + " M106 P1 S0 ; Turn off nozzle cooling fan", + " M623 ; End of calibration routine", + " M73 P4 R23 ; Update progress", + " G1 X-48.2 F3000 ; Move X back", + " M400 ; Wait for all movements to complete", + " M984 A0.1 E1 S1 F6.28466 H0.4 ; Apply extruder calibration parameters", + " M106 P1 S178 ; Set nozzle cooling fan to moderate speed", + " M400 S7 ; Wait for 7 seconds", + " G1 X-28.5 F18000 ; Move X for wipe", + " G1 X-48.2 F3000 ; Move X back", + " G1 X-28.5 F18000 ; Wipe motion", + " G1 X-48.2 F3000 ; Move X back", + " G1 X-28.5 F12000 ; Wipe and shake", + " G1 X-48.2 F3000 ; Move X back", + " M400 ; Wait for all movements to complete", + " M106 P1 S0 ; Turn off nozzle cooling fan", + "M623 ; End of extruder calibration process", + "M104 S170 ; Prepare to wipe nozzle", + "M106 S255 ; Turn on fan", + ";===== Mech Mode Fast Check Start =================", + "M1002 gcode_claim_action : 3", + "G1 X128 Y128 F20000 ; Move to check position", + "G1 Z5 F1200 ; Raise Z-axis", + "M400 P200 ; Wait for movements to complete", + "M970.3 Q1 A5 K0 O3 ; Execute fast check routine", + "M974 Q1 S2 P0 ; Confirm fast check step", + "M970.2 Q1 K1 W58 Z0.1 ; Additional fast check", + "M974 S2 ; Confirm step", + "G1 X128 Y128 F20000 ; Repeat move to check position", + "G1 Z5 F1200 ; Raise Z-axis", + "M400 P200 ; Wait for movements to complete", + "M970.3 Q0 A10 K0 O1 ; Execute another fast check routine", + "M974 Q0 S2 P0 ; Confirm step", + "M970.2 Q0 K1 W78 Z0.1 ; Another additional check", + "M974 S2 ; Confirm step", + "M975 S1 ; Confirm vibration suppression", + "G1 F30000 ; Set movement speed", + "G1 X0 Y5 ; Move to next step", + "G28 X ; Re-home XY", + "G1 Z4 F1200 ; Lower Z-axis", + ";===== Wipe Nozzle ================================", + "M1002 gcode_claim_action : 14", + "M975 S1 ; Enable wipe mode", + "M106 S255 ; Turn on fan (G28 turns off fan)", + "M211 S ; Push soft endstop status", + "M211 X0 Y0 Z0 ; Turn off Z-axis endstop", + ";===== Remove Waste by Touching Start =============", + "M104 S170 ; Set nozzle temp", + "M83 ; Set extruder to relative mode", + "G1 E-1 F500 ; Retract filament", + "G90 ; Set absolute positioning", + "M83 ; Set extruder to relative mode", + "M109 S170 ; Set nozzle temperature", + "G0 X108 Y-0.5 F30000 ; Move to wipe position", + "G380 S3 Z-5 F1200 ; Lower nozzle to wipe", + "; Repeat wipe sequence", + "G1 Z2 F1200", + "G1 X110 F10000", + "G380 S3 Z-5 F1200", + "M73 P22 R18", + "G1 Z2 F1200", + "G1 X112 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X114 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X116 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X118 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X120 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X122 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X124 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X126 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X128 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X130 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X132 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X134 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X136 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X138 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X140 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X142 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X144 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X146 F10000", + "G380 S3 Z-5 F1200", + "G1 Z2 F1200", + "G1 X148 F10000", + "G380 S3 Z-5 F1200", + "G1 Z5 F30000 ; Raise Z", + ";===== Remove Waste by Touching End ===============", + "G1 Z10 F1200 ; Lift nozzle", + "G0 X118 Y261 F30000 ; Move to safe position", + "G1 Z5 F1200 ; Lower Z", + "M109 S170 ; Set nozzle temperature", + "G28 Z P0 T300 ; Home Z with low precision", + "G29.2 S0 ; Turn off ABL", + "M104 S140 ; Prepare for auto bed leveling", + "G0 Z5 F20000 ; Lift Z", + "G0 X128 Y261 F20000 ; Move to exposed steel surface", + "G0 Z-1.01 F1200 ; Lower nozzle to stop position", + "G91 ; Set relative positioning", + "G2 I1 J0 X2 Y0 F2000.1 ; Execute wipe motion", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G90 ; Set absolute positioning", + "G1 Z10 F1200 ; Raise Z", + ";===== Brush Material Wipe Nozzle =================", + "G90 ; Set absolute positioning", + "G1 Y250 F30000 ; Move to brush position", + "G1 X55 ; Move X", + "G1 Z1.300 F1200 ; Lower nozzle", + "G1 Y262.5 F6000 ; Wipe motion", + "G91 ; Set relative positioning", + "G1 X-35 F30000 ; Move along brush", + "G1 Y-0.5", + "G1 X45", + "G1 Y-0.5", + "G1 X-45", + "G1 Y-0.5", + "G1 X45", + "G1 Y-0.5", + "G1 X-45", + "G1 Y-0.5", + "G1 X45", + "G1 Z5.000 F1200 ; Lift nozzle", + "G90 ; Set absolute positioning", + "G1 X30 Y250.000 F30000 ; Move to brush position", + "G1 Z1.300 F1200 ; Lower nozzle", + "G1 Y262.5 F6000 ; Wipe motion", + "G91 ; Set relative positioning", + "G1 X35 F30000 ; Move along brush", + "G1 Y-0.5", + "G1 X-45", + "G1 Y-0.5", + "G1 X45", + "G1 Y-0.5", + "G1 X-45", + "G1 Y-0.5", + "G1 X45", + "G1 Y-0.5", + "G1 X-45", + "G1 Z10.000 F1200 ; Lift nozzle", + ";===== Brush Material Wipe Nozzle End ============", + "G90 ; Set absolute positioning", + "G1 Y250 F30000 ; Move to safe position", + "G1 X138", + "G1 Y261", + "G0 Z-1.01 F1200 ; Stop nozzle", + "G91 ; Set relative positioning", + "G2 I1 J0 X2 Y0 F2000.1", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "M73 P23 R18", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "G2 I1 J0 X2", + "G2 I-0.75 J0 X-1.5", + "M109 S140 ; Set nozzle temperature", + "M106 S255 ; Turn on fan (G28 turns off fan)", + "M211 R ; Restore soft endstop status", + ";===== Bed Leveling ===============================", + "M1002 judge_flag g29_before_print_flag ; Check bed leveling flag", + "G90 ; Set absolute positioning", + "G1 Z5 F1200 ; Lift nozzle", + "G1 X0 Y0 F30000 ; Move to home position", + "G29.2 S1 ; Turn on ABL (auto bed level)", + "M190 S65 ; Set and wait bed temp", + "M109 S140 ; Set and wait nozzle temp", + "M106 S0 ; Turn off fan (reduce noise)", + "M622 J1", + " M1002 gcode_claim_action : 1", + " G29 A1 X115.2 Y115.2 I25.6 J25.6 ; Perform bed leveling", + " M400 ; Wait for all movements to complete", + " M500 ; Save calibration data", + "M623 ; End bed leveling", + ";===== Home After Wipe Mouth ======================", + "M1002 judge_flag g29_before_print_flag ; Check if bed leveling was performed", + "M622 J0 ; Signal start of sequence", + " M1002 gcode_claim_action : 13 ; Claim home action", + " G28 ; Home all axes", + "M623 ; End sequence", + ";===== Home After Wipe Mouth End ==================", + "G1 X108.000 Y-0.500 F30000 ; Move to start position", + "G1 Z0.300 F1200 ; Lower Z for calibration", + "M400 ; Wait for moves to complete", + "G2814 Z0.32 ; Set Z offset", + "M104 S220 ; Set nozzle temperature to 220°C for printing", + ";===== Nozzle Load Line ===========================", + ";G90 ; Set absolute positioning", + ";M83 ; Set extruder to relative mode", + ";G1 Z5 F1200 ; Raise Z before priming", + ";G1 X88 Y-0.5 F20000 ; Move to priming position", + ";G1 Z0.3 F1200 ; Lower nozzle for priming", + ";M109 S220 ; Wait for nozzle temperature to reach 220°C", + ";G1 E2 F300 ; Extrude filament", + ";G1 X168 E4.989 F6000 ; Extrude along the line", + ";G1 Z1 F1200 ; Lift Z after priming", + ";===== Extrude Calibration Test ====================", + "M400 ; Wait for all commands to complete", + "M900 S ; Start calibration", + "M900 C ; Start calibration cycle", + "G90 ; Set absolute positioning", + "M83 ; Set extruder to relative mode", + "M109 S220 ; Set and wait nozzle temp", + "G0 X128 E8 F904.991 ; Extrude calibration test pattern", + "G0 X133 E.3742 F1508.32", + "G0 X138 E.3742 F6033.27", + "G0 X143 E.3742 F1508.32", + "G0 X148 E.3742 F6033.27", + "G0 X153 E.3742 F1508.32", + "G91 ; Set relative positioning", + "G1 X1 Z-0.300 ; Lower nozzle slightly", + "G1 X4 ", + "G1 Z1 F1200 ; Raise back up", + "G90 ; Return to absolute positioning", + "M400 ; Wait for completion", + "M900 R ; Reset calibration", + "M1002 judge_flag extrude_cali_flag ; Check calibration status", + "M622 J1 ; Signal start of sequence", + " G90 ; Set absolute positioning", + " G1 X108.000 Y1.000 F30000 ; Move to calibration position", + " G91 ; Set relative positioning", + " G1 Z-0.700 F1200 ; Lower nozzle slightly", + " G90 ; Return to absolute positioning", + " M83 ; Set extruder to relative mode", + " G0 X128 E10 F904.991 ; Run extrusion test", + " G0 X133 E.3742 F1508.32", + " G0 X138 E.3742 F6033.27", + " G0 X143 E.3742 F1508.32", + " G0 X148 E.3742 F6033.27", + " G0 X153 E.3742 F1508.32", + " G91 ; Set relative positioning", + " G1 X1 Z-0.300 ; Lower slightly", + " G1 X4", + " G1 Z1 F1200 ; Raise back up", + " G90 ; Return to absolute positioning", + " M400 ; Wait for completion", + "M623 ; End sequence", + "G1 Z0.2 ; Lower nozzle slightly", + "M1002 gcode_claim_action : 0 ; Claim action", + "M400 ; Wait for actions to complete", + ";===== Final Print Prep ===========================", + "G29.1 Z-0.02 ; Adjust Z height for PEI plate", + "M960 S1 P0 ; Turn off laser", + "M960 S2 P0 ; Turn off laser", + "M106 S0 ; Turn off fan", + "M106 P2 S0 ; Turn off auxiliary fan", + "M106 P3 S0 ; Turn off chamber fan", + "M975 S1 ; Enable mechanical suppression mode", + "G90 ; Set absolute positioning", + "M83 ; Set extruder to relative mode", + "T1000 ; Tool selection", + "M211 X0 Y0 Z0 ; Disable soft endstops", + "M1007 S1 ; Enable mass estimation", + "G29.4 ; Start additional leveling routine", + "G90 ; Set absolute positioning", + "G21 ; Set units to millimeters", + "M83 ; Set extruder to relative mode", + "M106 P3 S200 ; Set chamber fan speed", + "M981 S1 P20000 ; Open spaghetti detector" + ], + "gcodePost": [ + "; ===== FOOTER ====================================", + "M204 S6000 ; Set acceleration to 6000 mm/s^2", + "M1003 S0 ; Disable power loss recovery", + "M106 S0 ; Turn off part cooling fan", + "M106 P2 S0 ; Turn off auxiliary cooling fan", + "M981 S0 P20000 ; Close spaghetti detector", + "M106 P3 S0 ; Turn off chamber fan", + "G392 S0 ; Turn off nozzle clog detection", + "M400 ; Wait for buffer to clear", + "G92 E0 ; Zero the extruder", + "G1 E-0.8 F1800 ; Retract filament by 0.8mm", + "G1 Z26.1 F900 ; Raise Z-axis slightly", + "G1 X0 Y128 F18000 ; Move to safe position", + "G1 X-13.0 F3000 ; Move to another safe position", + ";===== Timelapse Sequence ========================", + "M1002 judge_flag timelapse_record_flag", + "M622 J1", + " M400 P100", + " M971 S11 C11 O0", + " M991 S0 P-1 ; End timelapse at safe pos", + "M623 ; End conditional block", + ";===== End of Print ==============================", + "M140 S0 ; Turn off heated bed", + "M106 S0 ; Turn off all cooling fans", + "M106 P2 S0 ; Turn off auxiliary cooling fan", + "M106 P3 S0 ; Turn off chamber cooling fan", + ";===== Pull Back Filament ========================", + "M620 S255 ; Retract filament to AMS", + "G1 X267 F15000 ; Move to retract position", + "T255 ; Select tool 255 (AMS retract)", + "G1 X-28.5 F18000", + "G1 X-48.2 F3000", + "G1 X-28.5 F18000", + "G1 X-48.2 F3000", + "M621 S255 ; Complete filament unload", + "M104 S0 ; Turn off hotend", + "M400 ; Wait for all motion to complete", + "M17 S ; Enable steppers", + "M17 Z0.4 ; Reduce Z stepper motor current", + "G1 Z125.6 F600 ; Move Z-axis up", + "G1 Z123.6", + "M400 P100", + "M17 R ; Restore Z motor current", + "G90 ; Set absolute positioning", + "G1 X-48 Y180 F3600 ; Move to parking position", + "M220 S100 ; Reset feedrate to 100%", + "M201.2 K1.0 ; Reset acceleration magnitude", + "M73.2 R1.0 ; Reset remaining time estimation", + "M1002 set_gcode_claim_speed_level : 0", + ";===== Printer Finish Sound ======================", + "M17", + "M400 S1", + "M1006 S1", + "M1006 A0 B20 L100 C37 D20 M40 E42 F20 N60", + "M1006 A0 B10 L100 C44 D10 M60 E44 F10 N60", + "M1006 A0 B10 L100 C46 D10 M80 E46 F10 N80", + "M1006 A44 B20 L100 C39 D20 M60 E48 F20 N60", + "M1006 A0 B10 L100 C44 D10 M60 E44 F10 N60", + "M1006 A0 B10 L100 C0 D10 M60 E0 F10 N60", + "M1006 A0 B10 L100 C39 D10 M60 E39 F10 N60", + "M1006 A0 B10 L100 C0 D10 M60 E0 F10 N60", + "M1006 A0 B10 L100 C44 D10 M60 E44 F10 N60", + "M1006 A0 B10 L100 C0 D10 M60 E0 F10 N60", + "M1006 A0 B10 L100 C39 D10 M60 E39 F10 N60", + "M1006 A0 B10 L100 C0 D10 M60 E0 F10 N60", + "M1006 A0 B10 L100 C48 D10 M60 E44 F10 N80", + "M1006 A0 B10 L100 C0 D10 M60 E0 F10 N80", + "M1006 A44 B20 L100 C49 D20 M80 E41 F20 N80", + "M1006 A0 B20 L100 C0 D20 M60 E0 F20 N80", + "M1006 A0 B20 L100 C37 D20 M30 E37 F20 N60", + "M1006 W", + "M400 ; Wait for completion", + "M18 X Y Z ; Disable motors", + "M73 P100 R0 ; Set progress to 100%" + ], + "gcodeChange": [ + "; BEGIN tool change from {last_tool} to {tool}", + "M620 S{tool}A ; prepare tool change", + "M204 S9000 ; Set acceleration to 9000 mm/s^2", + "M620.11 S0 ; Reset filament retraction state", + "M400 ; wait for moves to complete", + "M620.1 E F187 T240 ; set purge rate 187 temp 240", + "T{tool} ; initiate tool change", + "M620.1 E F374 T240 ; set purge rate 374 temp 240", + "M620.11 S0 ; Reset filament retraction state", + "G92 E0 ; reset extruder position", + "M400 ; wait for moves to complete", + " ; -- PURGE --", + "M109 S240 ; Set nozzle temperature to 240°C", + "G1 E23.7 F187 ; Extrude 23.7mm of filament at flowrate 187", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F187 ; Continue extrusion at flowrate 187", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E-2 F1800 ; Retract filament by 2mm", + "G1 E2 F300 ; Recover retraction slowly", + "M400 ; Wait for all movements to complete", + "M109 S{temp} ; Set nozzle temperature back to print temp", + " ; -- SHAKE --", + "M106 P1 S255 ; Turn on nozzle cooling fan to full speed", + "M400 S3 ; Wait for all movements to complete", + "G1 X70 F5000 ; Move to X70", + "G1 X90 F3000 ; Move to X90", + "G1 Y255 F4000 ; Move to Y255", + "G1 X105 F5000 ; Move to X105", + "G1 Y265 ; Move to Y265", + "G1 X70 F10000 ; Rapid move to X70", + "G1 X100 F5000 ; Move to X100", + "G1 X70 F10000 ; Rapid move back to X70", + "G1 X100 F5000 ; Move to X100", + "G1 X70 F10000 ; Shake filament residue", + "G1 X80 F15000", + "G1 X60", + "G1 X80", + "G1 X60", + "G1 X80 ; Shake to put down garbage", + "G1 X100 F5000 ; Move to X100", + "G1 X165 F15000 ; Wipe and shake", + "G1 Y256 ; Move Y aside to prevent collision", + "M400 ; Wait for all movements to complete", + "M204 S10000 ; Set acceleration to 10000 mm/s^2", + "M621 S{tool}A ; complete tool change", + "; END tool change" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "new": false, + "deviceName": "Bambu A1", + "bedBelt": false, + "fwRetract": false, + "filamentSource": "direct", + "extras": { + "bbl": { + "luke": { + "host": "10.20.20.75", + "code": "20709223", + "serial": "01P09C482401767", + "modified": true + }, + "leia": { + "host": "10.20.20.76", + "code": "35129782", + "serial": "01P09C461603301", + "modified": true + }, + "vader": { + "host": "10.20.20.77", + "code": "17711811", + "serial": "01P00A413100516", + "modified": true + }, + "Scrappy": { + "host": "10.10.2.5", + "serial": "01S00A292500224", + "code": "38373335", + "modified": true + } + } + }, + "imageURL": "", + "deviceZMax": 0, + "gcodeTime": 1, + "gcodeFeature": [], + "profiles": [ + { + "processName": "Bambu PLA", + "sliceHeight": 0.2, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillType": "grid", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.5, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 60, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 0, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 20, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 0, + "firstLayerFanSpeed": 0, + "outputTemp": 220, + "outputBedTemp": 65, + "outputFanSpeed": 255, + "outputFeedrate": 120, + "outputFinishrate": 90, + "outputSeekrate": 200, + "outputShellMult": 1.2, + "outputFillMult": 1.2, + "outputSparseMult": 1.2, + "outputRetractDist": 2, + "outputRetractSpeed": 80, + "outputRetractWipe": 0, + "outputRetractDwell": 0, + "outputShortPoly": 100, + "outputMinSpeed": 5, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.3, + "firstLayerFillRate": 80, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 210, + "firstLayerBedTemp": 60, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "sliceLineWidth": 0, + "sliceFillRepeat": 2, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "outputAlternating": false, + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputFanLayer": 1, + "outputNozzle": 0, + "sliceAngle": 45, + "sliceZInterleave": false + } + ] +} \ No newline at end of file diff --git a/src2/kiri-dev/fdm/Bambu.P1S.json b/src2/kiri-dev/fdm/Bambu.P1S.json new file mode 100644 index 00000000..ea29084c --- /dev/null +++ b/src2/kiri-dev/fdm/Bambu.P1S.json @@ -0,0 +1,502 @@ +{ + "mode": "FDM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 256, + "bedDepth": 256, + "bedRound": false, + "maxHeight": 256, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M73 P{progress}" + ], + "gcodeLayer": [ + "; CHANGE_LAYER", + "; layer num/total_layer_count: {layer}/{layers}", + "M622.1 S1 ; for prev firware, default turned on", + "M1002 judge_flag timelapse_record_flag", + "M622 J1", + "M971 S11 C10 O0 ; timelapse without wipe tower", + "M623", + "M73 L{layer}", + "M991 S0 P{layer-1} ; notify layer change", + ";; IF { layer == 0 }", + "M204 S500", + ";; ELSE", + "M204 S5000", + ";; END" + ], + "gcodePre": [ + "; total layer number: {layers}", + "; model label id: {model_labels}", + ";; PREAMBLE", + ";; DEFINE BAMBU-AMS 0,1,2,3", + "; ===== HEADER ====================================", + "M73 P0 R14 ; Initial progress and time remaining", + "M201 X20000 Y20000 Z500 E5000 ; Set max acceleration", + "M203 X500 Y500 Z20 E30 ; Set max feedrates", + "M204 P20000 R5000 T20000 ; Set acceleration", + "M205 X9.00 Y9.00 Z3.00 E2.50 ; Set advanced settings", + "M106 S0 ; Set part fan speed to 0", + "M106 P2 S0 ; Turn off aux fan", + ";===== machine: P1S ===============================", + "M104 S75 ; Set extruder temp to turn on HB fan, prevent filament oozing", + "M710 A1 S255 ; Turn on MC fan by default (P1S)", + ";===== Reset machine status =======================", + "M290 X40 Y40 Z2.6666666 ; Set baby stepping offsets", + "G91 ; Set to relative positioning", + "M17 Z0.4 ; Lower Z-motor current", + "G380 S2 Z30 F300 ; Lower hotbed (G380 like G38)", + "G380 S2 Z-25 F300 ; Continue to lower bed", + "G1 Z5 F300 ; Raise bed by 5mm", + "G90 ; Set to absolute positioning", + "M17 X1.2 Y1.2 Z0.75 ; Reset motor current to default", + "M960 S5 P1 ; Turn on logo lamp", + "M220 S100 ; Reset feedrate to 100%", + "M221 S100 ; Reset flowrate to 100%", + "M73.2 R1.0 ; Reset remaining time magnitude", + "M1002 set_gcode_claim_speed_level : 5 ; Set G-code claim speed level", + "M221 X0 Y0 Z0 ; Turn off soft endstop to prevent logic issues", + "G29.1 Z0 ; Clear Z-trim value", + "M204 S10000 ; Initialize acceleration to 10 m/s^2", + ";===== Heatbed preheat ============================", + "M1002 gcode_claim_action : 2 ; Claim action for preheating", + "M140 S{bed_temp} ; Set bed temperature to 65°C", + "M190 S{bed_temp} ; Wait for bed temperature to reach 65°C", + ";===== Turn on fans to prevent PLA jamming ========", + "M106 P3 S180 ; set chamber fan speed to prevent PLA jamming", + "M106 P2 S100 ; Turn on aux fan to cool toolhead", + ";===== Prepare print temperature and material =====", + "M104 S{temp} ; Set extruder temp to 220°C", + "G91 ; Set to relative positioning", + "G0 Z10 F1200 ; Raise Z by 10mm", + "G90 ; Set to absolute positioning", + "G28 X ; Home X axis", + "M975 S1 ; Turn on feature (guess: vibration suppression)", + "M73 P31 R9 ; Update progress and time remaining", + "G1 X60 F12000 ; Fast move to X60", + "G1 Y245 ; Move to Y245", + "G1 Y265 F3000 ; Move to Y265 slowly", + "M620 M ; Custom command (guess: material management)", + "M620 S{tool}A ; Switch material if AMS present", + "M109 S{temp} ; Wait for extruder to reach 220°C", + "G1 X120 F12000 ; Fast move to X120", + "G1 X20 Y50 F12000 ; Fast move to X20 Y50", + "G1 Y-3 ; Move down Y-axis by 3", + "T{tool} ; Select starting tool", + "G1 X54 F12000 ; Fast move to X54", + "G1 Y265 ; Move to Y265", + "M400 ; Wait for all moves to finish", + "M621 S{tool}A ; Custom command", + "M620.1 E F523.843 T240 ; Extrude command with custom parameters", + "M412 S1 ; Turn on filament runout detection", + "M109 S250 ; Set nozzle to common flush temp", + "M106 P1 S0 ; Turn off nozzle fan", + "G92 E0 ; Reset extruder position", + "G1 E50 F200 ; Extrude 50mm of filament", + "M400 ; Wait for moves to finish", + "M104 S{temp} ; Set nozzle temperature to 220°C", + "G92 E0 ; Reset extruder position", + "G1 E50 F200 ; Extrude 50mm of filament", + "M400 ; Wait for moves to finish", + "M106 P1 S255 ; Turn on nozzle fan to full speed", + "G92 E0 ; Reset extruder position", + "G1 E5 F300 ; Extrude 5mm of filament", + "M109 S200 ; Drop nozzle temperature to 200°C to shrink filament", + "G92 E0 ; Reset extruder position", + "M73 P33 R9 ; Update progress and time remaining", + "G1 E-0.5 F300 ; Retract filament by 0.5mm", + "M73 P35 R9 ; Update progress and time remaining", + "G1 X70 F9000 ; Fast move to X70", + "G1 X76 F15000 ; Fast move to X76", + "G1 X65 F15000 ; Fast move to X65", + "G1 X76 F15000 ; Repeat movements to shake and clean nozzle", + "G1 X65 F15000 ; Shake to put down garbage", + "G1 X80 F6000 ; Move to X80", + "G1 X95 F15000 ; Fast move to X95", + "G1 X80 F15000 ; Return to X80", + "G1 X165 F15000 ; Wipe and shake", + "M400 ; Wait for all moves to finish", + "M106 P1 S0 ; Turn off nozzle fan", + ";===== Wipe nozzle ===============================", + "M1002 gcode_claim_action : 14 ; Claim action for nozzle wipe", + "M975 S1 ; Turn on feature (guess: vibration suppression)", + "M106 S255 ; Turn on part fan to full speed", + "G1 X65 Y230 F18000 ; Fast move to start of wipe", + "G1 Y264 F6000 ; Wipe along Y-axis", + "M109 S200 ; Set nozzle temperature to 200°C", + "G1 X100 F18000 ; Wipe first pass", + "G0 X135 Y253 F20000 ; Move to exposed steel surface edge", + "G28 Z P0 T300 ; Home Z with low precision, permit 300°C temperature", + "G29.2 S0 ; Turn off ABL (auto bed leveling)", + "G0 Z5 F20000 ; Raise Z by 5mm", + "G1 X60 Y265 ; Move to X60 Y265", + "G92 E0 ; Reset extruder position", + "G1 E-0.5 F300 ; Retract more", + "G1 X100 F5000 ; Second wipe pass", + "G1 X70 F15000 ; Repeat movements for further wiping", + "G1 X100 F5000", + "G1 X70 F15000", + "G1 X100 F5000", + "G1 X70 F15000", + "G1 X90 F5000", + "G0 X128 Y261 Z-1.5 F20000 ; Move to exposed steel surface, stop nozzle", + "M104 S140 ; Set temp down to heatbed acceptable", + "M106 S255 ; Turn on part fan (G28 turned off fan)", + "M221 S ; Push soft endstop status", + "M221 Z0 ; Turn off Z axis endstop", + "G0 Z0.5 F20000 ; Raise Z by 0.5mm", + "G0 X125 Y259.5 Z-1.01 ; Move to wiping position", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y262.5", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y260.0", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y262.0", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y260.5", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y261.5", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y261.0", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 X128", + "G2 I0.5 J0 F300 ; Arc movements for final cleaning", + "M73 P36 R8 ; Update progress and time remaining", + "G2 I0.5 J0 F300", + "G2 I0.5 J0 F300", + "G2 I0.5 J0 F300", + "M109 S140 ; Wait for nozzle temp to reach heatbed acceptable temp", + "G2 I0.5 J0 F3000 ; More arc movements", + "G2 I0.5 J0 F3000", + "G2 I0.5 J0 F3000", + "G2 I0.5 J0 F3000", + "M221 R ; Pop softend status", + "G1 Z10 F1200 ; Raise Z by 10mm", + "M400 ; Wait for all moves to finish", + "G1 Z10 ; Raise Z by 10mm", + "G1 F30000 ; Set fast feedrate", + "G1 X230 Y15 ; Move to X230 Y15", + "G29.2 S1 ; Turn on ABL", + "M106 S0 ; Turn off part fan, too noisy", + ";===== Bed leveling ===============================", + "M1002 judge_flag g29_before_print_flag ; Judge flag before leveling", + "M622 J1", + " M1002 gcode_claim_action : 1 ; Claim action for bed leveling", + " G29 A X115.2 Y115.2 I25.6 J25.6 ; Perform bed leveling", + " M400 ; Wait for all moves to finish", + " M500 ; Save calibration data", + "M623", + ";===== Home after wipe mouth ======================", + "M1002 judge_flag g29_before_print_flag ; Judge flag before homing", + "M622 J0", + " M1002 gcode_claim_action : 13 ; Claim action for homing", + " G28 ; Home all axes", + "M623", + ";===== Home after wipe mouth end ==================", + "M975 S1 ; Turn on vibration suppression", + ";===== Turn on fans to prevent PLA jamming ========", + "M106 P3 S180 ; set chamber fan speed to Prevent PLA jamming", + "M106 P2 S100 ; Turn on aux fan to cool toolhead", + "M104 S{temp} ; Set extruder temp to 220°C earlier, reduce wait time", + ";===== Mech mode fast check =======================", + "G1 X128 Y128 Z10 F20000 ; Move to center for check", + "M400 P200 ; Wait and perform check", + "M970.3 Q1 A7 B30 C80 H15 K0 ; Custom command (mechanical check)", + "M974 Q1 S2 P0 ; Custom command", + "G1 X128 Y128 Z10 F20000 ; Move again to center for check", + "M400 P200 ; Wait and perform another check", + "M970.3 Q0 A7 B30 C90 Q0 H15 K0 ; Custom command", + "M974 Q0 S2 P0 ; Custom command", + "M975 S1 ; Turn on feature (vibration suppression)", + "G1 F30000 ; Set fast feedrate", + "M73 P37 R8 ; Update progress and time remaining", + "G1 X230 Y15 ; Move to X230 Y15", + "G28 X ; Re-home X and Y", + ";===== Nozzle load line ===========================", + "M975 S1 ; Turn on vibration suppression", + "G90 ; Set to absolute positioning", + "M83 ; Set extruder to relative mode", + "T1000 ; Custom command", + "G1 X18.0 Y1.0 Z0.8 F18000 ; Move to start position", + "M109 S{temp} ; Set extruder temp to 220°C", + "G1 Z0.2 ; Lower Z to 0.2mm", + "G0 E2 F300 ; Extrude 2mm of filament", + "G0 X240 E25 F5539.96 ; Move to X240 while extruding", + "G0 Y15 E1.166 F1384.99 ; Move along Y while extruding", + "G0 X239.5 ; Move to X239.5", + "G0 E0.2 ; Extrude 0.2mm of filament", + "G0 Y1.5 E1.166 ; Move along Y while extruding", + "G0 X18 E25 F5539.96 ; Return to start position while extruding", + "M400 ; Wait for all moves to finish", + ";===== Final Preparations =========================", + ";G29.1 Z-0.04 ; Adjust Z-offset for textured PEI plate", + "M1002 gcode_claim_action : 0 ; Claim action", + "M106 S0 ; Turn off part fan", + "M106 P2 S0 ; Turn off aux fan", + "M106 P3 S0 ; Turn off chamber fan", + "M975 S1 ; Turn on mechanical mode suppression", + "G90 ; Set to absolute positioning", + "G21 ; Set units to millimeters", + "M83 ; Use relative distances for extrusion", + "M106 P3 S200 ; Set chamber fan speed to 200 (prevents PLA jamming)", + "M981 S1 P20000 ; Open spaghetti detector" + ], + "gcodePost": [ + "; ===== FOOTER =====================================", + "M106 S0 ; Turn off part cooling fan", + "M106 P2 S0 ; Turn off auxiliary cooling fan", + "M981 S0 P20000 ; Close spaghetti detector", + "M106 P3 S0 ; Turn off chamber fan", + "M400 ; Wait for buffer to clear", + "G92 E0 ; Zero the extruder", + "G1 E-0.8 F1800 ; Retract filament by 0.8mm", + "G91 ; relative moves", + "G1 Z5 F900 ; drop bed 5mm", + "G90 ; absolute moves", + "G1 X65 Y245 F12000 ; Move to safe position X65 Y245", + "G1 Y265 F3000 ; Move to Y265 slowly", + "G1 X65 Y245 F12000 ; Move back to X65 Y245", + "G1 Y265 F3000 ; Move to Y265 slowly again", + "M140 S0 ; Turn off heated bed", + "M106 S0 ; Turn off part cooling fan", + "M106 P2 S0 ; Turn off auxiliary cooling fan", + "M106 P3 S0 ; Turn off chamber cooling fan", + "G1 X100 F12000 ; Wipe nozzle", + "M620 S255 ; Pull back filament to AMS", + "G1 X20 Y50 F12000 ; Move to X20 Y50", + "G1 Y-3 ; Move down Y-axis by 3", + "T255 ; Tool change command (educated guess)", + "G1 X65 F12000 ; Move to X65", + "G1 Y265 ; Move to Y265", + "G1 X100 F12000 ; Wipe nozzle again", + "M621 S255 ; Custom command (educated guess)", + "M104 S0 ; Turn off hotend", + "M622.1 S1 ; Enable feature (default for older firmware)", + "M1002 judge_flag timelapse_record_flag ; Check if timelapse recording is needed", + "M622 J1 ; Enable timelapse", + " M400 ; Wait for all motions to complete", + " M991 S0 P-1 ; End smooth timelapse at safe position", + " M400 S3 ; Wait for last picture to be taken", + "M623 ; End of \"timelapse_record_flag\"", + "M400 ; Wait for all motions to complete", + "M17 S ; Engage stepper motors", + "M17 Z0.4 ; Lower Z motor current to reduce impact if obstruction exists", + "G1 Z{z_max} F600 ; Drop bed", + "M400 P100 ; Wait for 100ms", + "M17 R ; Restore Z motor current", + "M220 S100 ; Reset feedrate magnitude to 100%", + "M201.2 K1.0 ; Reset acceleration magnitude", + "M73.2 R1.0 ; Reset remaining time magnitude", + "M1002 set_gcode_claim_speed_level : 0 ; Set G-code claim speed level to 0", + "M17 X0.8 Y0.8 Z0.5 ; Lower motor current to 45% power", + "M73 P100 R0 ; Set progress to 100% with 0 remaining time" + ], + "gcodeChange": [ + "; BEGIN tool change from {last_tool} to {tool}", + "M620 S{tool}A ; prepare tool change", + "M204 S9000 ; Set acceleration to 9000 mm/s^2", + "M620.11 S0 ; Reset filament retraction state", + "M400 ; wait for moves to complete", + "M620.1 E F187 T240 ; set purge rate 187 temp 240", + "T{tool} ; initiate tool change", + "M620.1 E F374 T240 ; set purge rate 374 temp 240", + "M620.11 S0 ; Reset filament retraction state", + "G92 E0 ; reset extruder position", + "M400 ; wait for moves to complete", + " ; -- PURGE --", + "M109 S240 ; Set nozzle temperature to 240°C", + "G1 E23.7 F187 ; Extrude 23.7mm of filament at flowrate 187", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F187 ; Continue extrusion at flowrate 187", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E-2 F1800 ; Retract filament by 2mm", + "G1 E2 F300 ; Recover retraction slowly", + "M400 ; Wait for all movements to complete", + "M109 S{temp} ; Set nozzle temperature back to print temp", + " ; -- SHAKE --", + "M106 P1 S255 ; Turn on nozzle cooling fan to full speed", + "M400 S3 ; Wait for all movements to complete", + "G1 X70 F5000 ; Move to X70", + "G1 X90 F3000 ; Move to X90", + "G1 Y255 F4000 ; Move to Y255", + "G1 X105 F5000 ; Move to X105", + "G1 Y265 ; Move to Y265", + "G1 X70 F10000 ; Rapid move to X70", + "G1 X100 F5000 ; Move to X100", + "G1 X70 F10000 ; Rapid move back to X70", + "G1 X100 F5000 ; Move to X100", + "G1 X70 F10000 ; Shake filament residue", + "G1 X80 F15000", + "G1 X60", + "G1 X80", + "G1 X60", + "G1 X80 ; Shake to put down garbage", + "G1 X100 F5000 ; Move to X100", + "G1 X165 F15000 ; Wipe and shake", + "G1 Y256 ; Move Y aside to prevent collision", + "M400 ; Wait for all movements to complete", + "M204 S10000 ; Set acceleration to 10000 mm/s^2", + "M621 S{tool}A ; complete tool change", + "; END tool change" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "new": false, + "deviceName": "Bambu P1S", + "bedBelt": false, + "fwRetract": false, + "filamentSource": "direct", + "extras": { + "bbl": {} + }, + "imageURL": "", + "deviceZMax": 0, + "gcodeTime": 1, + "gcodeFeature": [], + "profiles": [ + { + "processName": "Bambu PLA", + "sliceHeight": 0.2, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.2, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 60, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 0, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 20, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 0, + "firstLayerFanSpeed": 0, + "outputTemp": 210, + "outputBedTemp": 60, + "outputFanSpeed": 255, + "outputFeedrate": 110, + "outputFinishrate": 90, + "outputSeekrate": 200, + "outputShellMult": 1.2, + "outputFillMult": 1.2, + "outputSparseMult": 1.2, + "outputRetractDist": 1, + "outputRetractSpeed": 80, + "outputRetractWipe": 0, + "outputRetractDwell": 0, + "outputShortPoly": 50, + "outputMinSpeed": 5, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.3, + "firstLayerFillRate": 80, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 220, + "firstLayerBedTemp": 65, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "sliceLineWidth": 0, + "sliceFillRepeat": 2, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": false, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "outputAlternating": false, + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputFanLayer": 1, + "outputNozzle": 0, + "sliceAngle": 45, + "sliceZInterleave": false + } + ] +} diff --git a/src2/kiri-dev/fdm/Creality.CR-10S-Pro.json b/src2/kiri-dev/fdm/Creality.CR-10S-Pro.json new file mode 100644 index 00000000..8fe970f2 --- /dev/null +++ b/src2/kiri-dev/fdm/Creality.CR-10S-Pro.json @@ -0,0 +1,51 @@ +{ + "pre":[ + "M140 S{bed_temp} ; Set bed temp", + "M105 ; Report Temps", + "M190 S{bed_temp} ; Wait for bed to reach target", + "M104 S{temp} ; Set hot end temp", + "M105 ; Report Temps", + "M109 S{temp} ; Wait for hot end to reach target", + "M82 ; Absolute extrusion mode", + "G28 ; Home", + "G29 ; Auto bed Level", + "G92 E0 ; Reset Extruder", + "G1 Z2.0 F3000 ; Move Z Axis up", + "G1 X10.1 Y20 Z0.28 F5000.0 ; Move to start position", + "G1 X10.1 Y200.0 Z0.28 F1500.0 E15 ; Draw the first line", + "G1 X10.4 Y200.0 Z0.28 F5000.0 ; Move to side a little", + "G1 X10.4 Y20 Z0.28 F1500.0 E30 ; Draw the second line", + "G92 E0 ; Reset Extruder", + "G1 Z2.0 F3000 ; Move Z Axis up", + "G92 E0 ; Reset Extruder" + ], + "post":[ + "M107 ; Turn off cooling fan", + "G91 ; Relative positioning", + "G1 E-2 F2700 ; Retract", + "G1 Z10 ; Raise Z", + "G90 ; Absolute positioning", + "G1 X0 Y300 ; Eject print", + "M104 S0 ; Turn-off hot end", + "M140 S0 ; Turn-off bed", + "M84 X Y E ; Disable steppers except Z" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 300, + "bed_depth": 300, + "build_height": 400 + } +} diff --git a/src2/kiri-dev/fdm/Creality.CR-10S.json b/src2/kiri-dev/fdm/Creality.CR-10S.json new file mode 100644 index 00000000..e51cc961 --- /dev/null +++ b/src2/kiri-dev/fdm/Creality.CR-10S.json @@ -0,0 +1,50 @@ +{ + "pre":[ + "M140 S{bed_temp} ; Set bed temp", + "M105 ; Report Temps", + "M190 S{bed_temp} ; Wait for bed to reach target", + "M104 S{temp} ; Set hot end temp", + "M105 ; Report Temps", + "M109 S{temp} ; Wait for hot end to reach target", + "M82 ; Absolute extrusion mode", + "G28 ; Home", + "G92 E0 ; Reset Extruder", + "G1 Z2.0 F3000 ; Move Z Axis up", + "G1 X10.1 Y20 Z0.28 F5000.0 ; Move to start position", + "G1 X10.1 Y200.0 Z0.28 F1500.0 E15 ; Draw the first line", + "G1 X10.4 Y200.0 Z0.28 F5000.0 ; Move to side a little", + "G1 X10.4 Y20 Z0.28 F1500.0 E30 ; Draw the second line", + "G92 E0 ; Reset Extruder", + "G1 Z2.0 F3000 ; Move Z Axis up", + "G92 E0 ; Reset Extruder" + ], + "post":[ + "M107 ; Turn off cooling fan", + "G91 ; Relative positioning", + "G1 E-2 F2700 ; Retract", + "G1 Z10 ; Raise Z", + "G90 ; Absolute positioning", + "G1 X0 Y300 ; Eject print", + "M104 S0 ; Turn-off hot end", + "M140 S0 ; Turn-off bed", + "M84 X Y E ; Disable steppers except Z" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 300, + "bed_depth": 300, + "build_height": 400 + } +} diff --git a/src2/kiri-dev/fdm/Creality.CR-30.json b/src2/kiri-dev/fdm/Creality.CR-30.json new file mode 100644 index 00000000..8c0830d6 --- /dev/null +++ b/src2/kiri-dev/fdm/Creality.CR-30.json @@ -0,0 +1,172 @@ +{ + "pre":[ + "G28 ; home axes", + "G1 Y50 F1000 ; lift y axis off belt for heating", + "M107 ; turn off filament cooling fan", + "M140 S{bed_temp} ; set bed temp", + "M104 S{temp} ; set hot end temp", + "M105 ; report temps", + "M190 S{bed_temp} ; wait for bed", + "M105 ; report temps", + "M109 S{temp} ; wait for hot end", + "M82 ; absolute extrusion mode", + "G90 ; absolute positioning mode", + "G21 ; use metric values", + "G92 E0 Z0 ; zero E/Z axes", + "G1 Y0.15 ; move head to belt", + "G1 X200 E35 F800 ; extruder purge line", + "G1 Z0.3 ; shift belt away 0.3mm", + "G1 X0 E70 ; extruder purge line", + "G92 Z0 E0 ; reset bed + extruder position", + "M117 CR-30 Printing..." + ], + "post":[ + "M107 ; disable cooling fan", + "M106 P0 S0 ; disable cooling fan", + "M104 S0 ; shut down hot end", + "M140 S0 ; shut down bed", + "G92 E1 ; set filament position", + "G1 E-1 F300 ; retract filament", + "G28 X ; home X axis", + "G1 Y50 F1000 ; lift y axis", + "M84 ; disable stepper motors" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}", + "progress": "M117 {progress}% layer {layer} / {layers}", + "layer": [ + ";; --- layer {layer} {height} @ {z} ---", + "M117 {progress}% layer {layer} / {layers}" + ] + }, + "settings":{ + "image_url": "/decal/3dprintmill.png", + "image_anchor": 0, + "image_scale": 1, + "origin_center": false, + "extrude_abs": true, + "bed_belt": true, + "bed_width": 220, + "bed_depth": 350, + "build_height": 170 + }, + "profiles":[{ + "processName": "CR30 PLA", + "sliceHeight": 0.20, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.3, + "sliceFillSparse": 0.2, + "sliceFillType": "linear", + "sliceFillRepeat": 3, + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 5, + "sliceSolidLayers": 3, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1.0, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 0, + "firstLayerFanSpeed": 0, + "outputLoops": 0, + "outputTemp": 210, + "outputFanMax": 255, + "outputBedTemp": 70, + "outputFanSpeed": 255, + "outputFeedrate": 30, + "outputFinishrate": 20, + "outputSeekrate": 80, + "outputShellMult": 1.15, + "outputFillMult": 1.15, + "outputSparseMult": 1.15, + "outputFanLayer": 0, + "outputRetractDist": 6, + "outputRetractSpeed": 70, + "outputRetractWipe": 0, + "outputRetractDwell": 0, + "outputShortPoly": 50.0, + "outputMinSpeed": 5.0, + "outputCoastDist": 0.3, + "outputWipeDistance": 0, + "outputLayerRetract": false, + "detectThinWalls": false, + "zHopDistance": 0, + "antiBacklash": 0 + },{ + "processName": "CR30 PETG", + "sliceHeight": 0.20, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.3, + "sliceFillSparse": 0.2, + "sliceFillType": "linear", + "sliceFillRepeat": 3, + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 5, + "sliceSolidLayers": 3, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1.0, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 0, + "firstLayerFanSpeed": 0, + "outputLoops": 0, + "outputTemp": 240, + "outputFanMax": 0, + "outputBedTemp": 70, + "outputFanSpeed": 0, + "outputFeedrate": 30, + "outputFinishrate": 30, + "outputSeekrate": 80, + "outputShellMult": 1.15, + "outputFillMult": 1.15, + "outputSparseMult": 1.15, + "outputFanLayer": 0, + "outputRetractDist": 6, + "outputRetractSpeed": 70, + "outputRetractWipe": 0, + "outputRetractDwell": 0, + "outputShortPoly": 50.0, + "outputMinSpeed": 5.0, + "outputCoastDist": 0.3, + "outputWipeDistance": 0, + "outputLayerRetract": false, + "detectThinWalls": false, + "zHopDistance": 0, + "antiBacklash": 0 + }] +} diff --git a/src2/kiri-dev/fdm/Creality.Ender.3.json b/src2/kiri-dev/fdm/Creality.Ender.3.json new file mode 100644 index 00000000..99c2be1f --- /dev/null +++ b/src2/kiri-dev/fdm/Creality.Ender.3.json @@ -0,0 +1,153 @@ +{ + "deviceName": "Creality Ender 3", + "new": false, + "mode": "FDM", + "internal": 0, + "bedWidth": 220, + "bedDepth": 220, + "maxHeight": 300, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "originCenter": false, + "extrudeAbs": true, + "spindleMax": 0, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M73 P{progress}" + ], + "gcodeLayer": [ + ";; LAYER {layer} of {layers}", + "M117 Layer {layer} of {layers}" + ], + "gcodePre": [ + "M104 S160 ; set extruder temp", + "M140 S{bed_temp} ; set bed temp", + "M190 S{bed_temp} ; wait for bed temp", + "G28 W ; home all without mesh bed level", + "G80 ; mesh bed leveling", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "G1 F1000 ; set feedrate", + "G1 Y-3.0 Z0.5 ; park outside print area", + "M104 S{temp} ; set extruder actual required temp", + "M109 S{temp} ; wait for extruder actual required temp", + "G1 X60.0 E9.0 ; purge line", + "G1 X100.0 E12.5 ; purge line", + "M117 Printing" + ], + "gcodePost": [ + "G0 E-1 F900 ; retract slightly", + "M104 S0 ; turn off nozzle heat", + "M140 S0 ; turn off bed heat", + "M107 ; turn off fan", + "G0 F3000 ; set feedrate", + "G0 Y210 Z210 ; serve up print", + "G4 ; wait", + "M84 ; disable steppers", + "M117 Print Done" + ], + "gcodeProc": "", + "gcodeDwell": [], + "gcodeSpindle": [], + "gcodeChange": [], + "gcodeFExt": "gcode", + "gcodeSpace": true, + "gcodeStrip": false, + "gcodeLaserOn": [], + "gcodeLaserOff": [], + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "fwRetract": false, + "profiles": [ + { + "processName": "Ender 3 PLA", + "sliceHeight": 0.2, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "center", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 5, + "sliceSolidLayers": 2, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 200, + "firstLayerFanSpeed": 0, + "outputTemp": 205, + "outputBedTemp": 70, + "outputFanSpeed": 255, + "outputFeedrate": 80, + "outputFinishrate": 30, + "outputSeekrate": 150, + "outputShellMult": 1.35, + "outputFillMult": 1.3, + "outputSparseMult": 1.3, + "outputRetractDist": 4, + "outputRetractSpeed": 60, + "outputRetractWipe": 2, + "outputRetractDwell": 0, + "outputShortPoly": 50, + "outputMinSpeed": 1, + "outputCoastDist": 0.05, + "outputLayerRetract": false, + "zHopDistance": 0.1, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.2, + "firstLayerFillRate": 35, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "outputLoops": 0, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "sliceFillRepeat": 4, + "sliceLineWidth": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off" + } + ] +} diff --git a/src2/kiri-dev/fdm/Creality.Ender.5.json b/src2/kiri-dev/fdm/Creality.Ender.5.json new file mode 100644 index 00000000..8b496298 --- /dev/null +++ b/src2/kiri-dev/fdm/Creality.Ender.5.json @@ -0,0 +1,38 @@ +{ + "pre":[ + "M107 ; turn off filament cooling fan", + "G90 ; set absolute positioning mode", + "M82 ; set absolute positioning for extruder", + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G28 ; home axes", + "G92 E0 ; reset extruder position", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "M190 S{bed_temp} T{tool} ; wait for bed to reach target temp" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G28 XY ; home XY", + "M84 ; disable stepper motors" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 220, + "bed_depth": 220, + "build_height": 300 + } +} diff --git a/src2/kiri-dev/fdm/Creality.Ender.6.json b/src2/kiri-dev/fdm/Creality.Ender.6.json new file mode 100644 index 00000000..80968142 --- /dev/null +++ b/src2/kiri-dev/fdm/Creality.Ender.6.json @@ -0,0 +1,38 @@ +{ + "pre":[ + "M107 ; turn off filament cooling fan", + "M82 ; set absolute positioning for extruder", + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G28 ; home axes", + "G92 E0 ; reset extruder position", + "G0 Z2 ; park Z at 2mm over bed for heating", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "M190 S{bed_temp} T{tool} ; wait for bed to reach target temp" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G28 XY ; home XY", + "M84 ; disable stepper motors" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 250, + "bed_depth": 250, + "build_height": 400 + } +} diff --git a/src2/kiri-dev/fdm/Creality.K1.json b/src2/kiri-dev/fdm/Creality.K1.json new file mode 100644 index 00000000..0a1f353c --- /dev/null +++ b/src2/kiri-dev/fdm/Creality.K1.json @@ -0,0 +1,51 @@ +{ + "mode": "FDM", + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedWidth": 220, + "bedDepth": 220, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "gcodeTime": 1, + "maxHeight": 250, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeFeature": [], + "gcodeTrack": [], + "gcodeLayer": [], + "gcodePre": [ + "M140 S0", + "M104 S0 ", + "START_PRINT EXTRUDER_TEMP=220.000000 BED_TEMP=45.000000", + "G92 E0", + "G1 F2400 E-0.5" + ], + "gcodePost": [ + "M106 S0", + "M106 P2 S0", + "M106 P1 S0", + "END_PRINT", + "M82 ;absolute extrusion mode", + "M104 S0" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "new": false, + "deviceName": "Creality K1", + "fwRetract": false, + "filamentSource": "direct", + "profiles": [], + "extras": {} +} diff --git a/src2/kiri-dev/fdm/Creality.Sermoon.D1.json b/src2/kiri-dev/fdm/Creality.Sermoon.D1.json new file mode 100644 index 00000000..7dba459d --- /dev/null +++ b/src2/kiri-dev/fdm/Creality.Sermoon.D1.json @@ -0,0 +1,152 @@ +{ + "pre":[ + "M107 ; turn off filament cooling fan", + "G90 ; set absolute positioning mode", + "M82 ; set absolute positioning for extruder", + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G28 ; home axes", + "G92 E0 ; reset extruder position", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "M190 S{bed_temp} T{tool} ; wait for bed to reach target temp" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G28 XY ; home XY", + "M84 ; disable stepper motors" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 280, + "bed_depth": 260, + "build_height": 310 + }, + "profiles":[{ + "processName": "Sermoon PLA", + "sliceHeight": 0.25, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.2, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceSolidLayers": 3, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerRate": 20, + "firstLayerFillRate": 35, + "firstLayerPrintMult": 1.10, + "firstLayerLineMult": 1.0, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 210, + "outputFanMax": 255, + "outputBedTemp": 70, + "outputFanSpeed": 255, + "outputFeedrate": 55, + "outputFinishrate": 50, + "outputSeekrate": 80, + "outputShellMult": 1.2, + "outputFillMult": 1.3, + "outputSparseMult": 1.3, + "outputBrimCount": 2, + "outputBrimOffset": 2, + "outputRetractDist": 1, + "outputRetractSpeed": 50, + "outputRetractWipe": 0, + "outputRetractDwell": 10, + "outputShortPoly": 50.0, + "outputMinSpeed": 10.0, + "outputCoastDist": 0, + "outputLayerRetract": false, + "detectThinWalls": false, + "zHopDistance": 0, + "antiBacklash": 0 + },{ + "processName": "Sermoon PETG", + "sliceHeight": 0.25, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.2, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceSolidLayers": 3, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerRate": 20, + "firstLayerFillRate": 35, + "firstLayerPrintMult": 1.10, + "firstLayerLineMult": 1.0, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 230, + "outputFanMax": 255, + "outputBedTemp": 85, + "outputFanSpeed": 255, + "outputFeedrate": 55, + "outputFinishrate": 50, + "outputSeekrate": 100, + "outputShellMult": 1.2, + "outputFillMult": 1.3, + "outputSparseMult": 1.3, + "outputBrimCount": 2, + "outputBrimOffset": 2, + "outputRetractDist": 1.5, + "outputRetractSpeed": 50, + "outputRetractWipe": 0, + "outputRetractDwell": 10, + "outputShortPoly": 50.0, + "outputMinSpeed": 10.0, + "outputCoastDist": 0, + "outputLayerRetract": false, + "detectThinWalls": false, + "zHopDistance": 0, + "antiBacklash": 0 + }]} diff --git a/src2/kiri-dev/fdm/Cubicon.Single+.json b/src2/kiri-dev/fdm/Cubicon.Single+.json new file mode 100644 index 00000000..273b3850 --- /dev/null +++ b/src2/kiri-dev/fdm/Cubicon.Single+.json @@ -0,0 +1,37 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "M190 S{bed_temp} T{tool} ; set bed temperature", + "M109 S{temp} T{tool} ; set extruder temperature", + "M901 S35 ; set chamber temperature to 35 (standard)", + "G90 ; set absolute positioning mode", + "M82 ; set absolute positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 ; home axes", + "G29 ; level bed", + "G92 E0 ; reset all axes positions", + "G1 X0 Y0 Z0.25 F180 ; move xy to 0,0 and z 0.25mm over bed", + "G92 E0 ; zero the extruder", + "G1 E15 F200 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 ; turn off bed", + "M904 ; park extruder", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "file-ext": "hvs", + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 240, + "bed_depth": 190, + "build_height": 200 + } +} diff --git a/src2/kiri-dev/fdm/Cubicon.Style.json b/src2/kiri-dev/fdm/Cubicon.Style.json new file mode 100644 index 00000000..e0b14403 --- /dev/null +++ b/src2/kiri-dev/fdm/Cubicon.Style.json @@ -0,0 +1,32 @@ +{ + "pre":[ + "T{tool} ; select extruder #0", + "M104 S{temp} ; set nozzle temp", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "M109 S{temp} ; wait for nozzle to heat", + "S210 T{tool} ; wait for heating?", + "M82 ; absolute extrusion mode", + "G28 ; home", + "G1 Z15 F6000 ; lower platform 15mm", + "G92 E0 ; zero out extruder position", + "G1 F200 E3 ; prime the extruder 3mm" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 ; turn off bed", + "M904 ; park extruder", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "file-ext": "hvs", + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 150, + "bed_depth": 150, + "build_height": 150 + } +} diff --git a/src2/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json b/src2/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json new file mode 100644 index 00000000..d5c8a5b9 --- /dev/null +++ b/src2/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json @@ -0,0 +1,166 @@ +{ + "mode": "FDM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 205, + "bedDepth": 205, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "gcodeTime": 1, + "maxHeight": 205, + "originCenter": false, + "gcodeFan": [], + "gcodeFeature": [], + "gcodeTrack": [], + "gcodeLayer": [], + "gcodePre": [ + "G90", + "M106 S255", + "G28 X Y", + "G1 X50", + "M109 R90", + "G28", + "M104 S{temp}", + "G29", + "M107", + "G1 X100 Y20 F3000", + "G1 Z0.5", + "M109 S{temp}", + "M82", + "G92 E0", + "G1 F200 E10", + "G92 E0", + "G1 Z3", + "G1 F6000" + ], + "gcodePost": [ + "M106 S255", + "M140 S0", + "G91", + "G1 E-1 F300", + "G1 Z+3 F3000", + "G90", + "G28 X Y", + "M107", + "M84" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "new": false, + "deviceName": "Dagoma Disco Ult V2", + "imageURL": "", + "fwRetract": false, + "filamentSource": "direct", + "profiles": [ + { + "processName": "dagoma draft", + "sliceHeight": 0.2, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceLineWidth": 0, + "sliceFillGrow": 0, + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSolidRate": 0, + "sliceFillType": "hex", + "sliceSupportDensity": 0.15, + "sliceSupportOffset": 0, + "sliceSupportGap": 1, + "sliceSupportSize": 2, + "sliceSupportArea": 0.1, + "sliceSupportSpan": 5, + "sliceSupportGrow": 0, + "sliceSupportExtra": 0, + "sliceSupportAngle": 60, + "sliceSupportNozzle": 0, + "sliceSupportEnable": false, + "sliceSupportOutline": false, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstSliceHeight": 0.05, + "firstLayerRate": 17, + "firstLayerFillRate": 17, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1, + "firstLayerYOffset": 0, + "firstLayerNozzleTemp": 205, + "firstLayerBedTemp": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFanSpeed": 0, + "firstLayerFlatten": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputDraftShield": false, + "outputTemp": 220, + "outputBedTemp": 0, + "outputFeedrate": 60, + "outputFinishrate": 60, + "outputSeekrate": 120, + "outputShellMult": 1, + "outputFillMult": 1, + "outputSparseMult": 1, + "outputFanSpeed": 255, + "outputRetractDist": 3.5, + "outputRetractSpeed": 50, + "outputRetractWipe": 0, + "outputRetractDwell": 20, + "outputBrimCount": 2, + "outputBrimOffset": 2, + "outputShortPoly": 100, + "outputMinSpeed": 10, + "outputCoastDist": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputAvoidGaps": true, + "outputAlternating": false, + "outputLayerRetract": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "sliceDetectThin": "off", + "sliceMinHeight": 0.2, + "sliceAdaptive": false, + "zHopDistance": 0.2, + "arcTolerance": 0, + "antiBacklash": 1, + "ranges": [], + "outputFanLayer": 1, + "sliceAngle": 45, + "outputNozzle": 0 + } + ], + "extras": { + "palette": { + "printer": "ee5dd1884e2d", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + } +} \ No newline at end of file diff --git a/src2/kiri-dev/fdm/DeltaMaker.2T.json b/src2/kiri-dev/fdm/DeltaMaker.2T.json new file mode 100644 index 00000000..3520c616 --- /dev/null +++ b/src2/kiri-dev/fdm/DeltaMaker.2T.json @@ -0,0 +1,31 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "G21 ; Make sure we're in metric mode", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "G28 ; home axes", + "G92 E0 ; zero the extruded", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G1 F200 E3.5 ; set feed speed, prime nozzle", + "G92 E0 ; zero the extruded", + "G1 Z30 ; position near the bed before first position" + ], + "post":[ + "G28 ; home axes", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "nozzle_size": 0.5, + "origin_center": true, + "bed_width": 240, + "bed_depth": 240, + "bed_circle": true, + "build_height": 480 + } +} diff --git a/src2/kiri-dev/fdm/DeltaMaker.Original.json b/src2/kiri-dev/fdm/DeltaMaker.Original.json new file mode 100644 index 00000000..b079deac --- /dev/null +++ b/src2/kiri-dev/fdm/DeltaMaker.Original.json @@ -0,0 +1,31 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "G21 ; Make sure we're in metric mode", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "G28 ; home axes", + "G92 E0 ; zero the extruded", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G1 F200 E3.5 ; set feed speed, prime nozzle", + "G92 E0 ; zero the extruded", + "G1 Z30 ; position near the bed before first position" + ], + "post":[ + "G28 ; home axes", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "nozzle_size": 0.35, + "origin_center": true, + "bed_width": 240, + "bed_depth": 240, + "bed_circle": true, + "build_height": 260 + } +} diff --git a/src2/kiri-dev/fdm/FLSUN.QQ-S.json b/src2/kiri-dev/fdm/FLSUN.QQ-S.json new file mode 100644 index 00000000..be6c2689 --- /dev/null +++ b/src2/kiri-dev/fdm/FLSUN.QQ-S.json @@ -0,0 +1,40 @@ +{ + "pre":[ + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "M190 S{bed_temp} T0 ; wait for bed to reach target temp", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "M105 ; report temperatures", + "M82 ; absolute extrusion mode", + "G21 ; metric values", + "G90 ; absolute positioning", + "M82 ; set extruder to absolute mode", + "M107 ; start with the fan off", + "G28 ; Home all axes (max endstops)", + "G1 Z15.0 F9000 ; move the platform down 15mm", + "G92 E0 ; zero the extruded length", + "G1 F200 E3 ; extrude 3mm of feed stock", + "G92 E0 ; zero the extruded length again", + "G1 F9000 ; set default move speed", + "M117 Printing...", + "G92 E0" + ], + "post":[ + "G28 ; home axes", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "M107 ; fan off", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "nozzle_size": 0.4, + "origin_center": true, + "bed_width": 260, + "bed_depth": 260, + "bed_circle": true, + "build_height": 320 + } +} diff --git a/src2/kiri-dev/fdm/Flashforge.Finder.json b/src2/kiri-dev/fdm/Flashforge.Finder.json new file mode 100644 index 00000000..258b6f9a --- /dev/null +++ b/src2/kiri-dev/fdm/Flashforge.Finder.json @@ -0,0 +1,29 @@ +{ + "pre":[ + "M118 X15.00 Y15.00 Z10.00 T{tool}", + "M104 S{temp} T{tool}", + "M107", + "G90", + "M132 X Y Z A B", + "M907 X100 Y100 Z40 A80 B20", + "; pre-extrude", + "G1 X-75 Y-75 Z0.35 F400", + "G1 Y75 E30 F1800" + ], + "post":[ + "M107", + "M104 S0 T{tool}", + "G162 Z", + "M18" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": true, + "extrude_abs": true, + "bed_width": 140, + "bed_depth": 140, + "build_height": 140 + } +} diff --git a/src2/kiri-dev/fdm/Flashforge.Guider.II.json b/src2/kiri-dev/fdm/Flashforge.Guider.II.json new file mode 100644 index 00000000..05f409c9 --- /dev/null +++ b/src2/kiri-dev/fdm/Flashforge.Guider.II.json @@ -0,0 +1,43 @@ +{ + "pre":[ + "; set bed and nozzle temps", + "M104 S{temp} T{tool}", + "M140 S{bed_temp}", + "; absolute positioning and home", + "G90", + "G28", + "; wait for nozzle and bed to reach target temps", + "M190 S{bed_temp}", + "M109 S{temp} T{tool}", + "M7 T{tool}", + "M6 T{tool}", + "; select nozzle 0", + "M108 T{tool}", + "G92 E0", + "G1 E0 F300", + "; purge nozzle", + "G1 X150 Y75 Z15 E-2 F500", + "G1 Y125 Z0.5 F600", + "G1 Y50 E40 F600" + ], + "post":[ + "; stop fan, cool nozzle and bed", + "M107", + "M104 S0 T{tool}", + "M140 S0", + "; slow descent of bed", + "G0 F300", + "; end print", + "M18" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": true, + "extrude_abs": true, + "bed_width": 280, + "bed_depth": 250, + "build_height": 300 + } +} diff --git a/src2/kiri-dev/fdm/FolgerTech.FT5.json b/src2/kiri-dev/fdm/FolgerTech.FT5.json new file mode 100644 index 00000000..a4267d73 --- /dev/null +++ b/src2/kiri-dev/fdm/FolgerTech.FT5.json @@ -0,0 +1,39 @@ +{ + "pre":[ + "M117 Heating", + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 ; home axes", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 X0 Y5 Z3 ; move XY to 0,5 and Z 3mm over bed", + "M190 S{bed_temp} T{tool} ; wait for bed to reach target temp", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G0 Z0.25 ; position 0.25mm over bed", + "M117 Purge Nozzle ; announce purge", + "G1 X100 E30 F600 ; purge 30mm from extruder" + ], + "post":[ + "M117 Done ; announce done", + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off extruder", + "M140 S0 T{tool} ; turn off bed", + "G91 ; relative movement", + "G1 Z10 F1200 ; drop bed 10mm", + "G90 ; absolute positioning", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}", + "progress": "M117 {progress}% layer {layer} / {layers}" + }, + "settings":{ + "origin_center": false, + "bed_width": 300, + "bed_depth": 300, + "build_height": 300 + } +} diff --git a/src2/kiri-dev/fdm/GridBot.One.json b/src2/kiri-dev/fdm/GridBot.One.json new file mode 100644 index 00000000..21ccb365 --- /dev/null +++ b/src2/kiri-dev/fdm/GridBot.One.json @@ -0,0 +1,43 @@ +{ + "pre":[ + "M117 Heating ; announce heating", + "M107 ; turn off filament cooling fan", + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "G28 ; home axes", + "G1 F3000 ; set default move speed", + "M190 S{bed_temp} T0 ; wait for bed to reach target temp", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "M117 Purge ; announce purge", + "G92 E0 ; set extruder position to 0", + "G1 E20 F250 ; purge 20mm from extruder", + "G1 X0 Y0 Z0 E1 F3000 ; move to origin / wipe", + "M117 Start Print ; announce start of print" + ], + "post":[ + "M117 Done ; announce done", + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off extruder", + "M140 S0 T0 ; turn off bed", + "G91 ; relative movement", + "G1 Z10 F1200 ; drop bed 10mm", + "G90 ; absolute positioning", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}", + "progress": "M117 {progress}% layer {layer} / {layers}" + }, + "settings":{ + "image_url": "/decal/gridbot.png", + "image_anchor": 0, + "image_scale": 1, + "origin_center": false, + "bed_width": 300, + "bed_depth": 300, + "build_height": 300 + } +} diff --git a/src2/kiri-dev/fdm/GridBot.Two.json b/src2/kiri-dev/fdm/GridBot.Two.json new file mode 100644 index 00000000..21ccb365 --- /dev/null +++ b/src2/kiri-dev/fdm/GridBot.Two.json @@ -0,0 +1,43 @@ +{ + "pre":[ + "M117 Heating ; announce heating", + "M107 ; turn off filament cooling fan", + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "G28 ; home axes", + "G1 F3000 ; set default move speed", + "M190 S{bed_temp} T0 ; wait for bed to reach target temp", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "M117 Purge ; announce purge", + "G92 E0 ; set extruder position to 0", + "G1 E20 F250 ; purge 20mm from extruder", + "G1 X0 Y0 Z0 E1 F3000 ; move to origin / wipe", + "M117 Start Print ; announce start of print" + ], + "post":[ + "M117 Done ; announce done", + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off extruder", + "M140 S0 T0 ; turn off bed", + "G91 ; relative movement", + "G1 Z10 F1200 ; drop bed 10mm", + "G90 ; absolute positioning", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}", + "progress": "M117 {progress}% layer {layer} / {layers}" + }, + "settings":{ + "image_url": "/decal/gridbot.png", + "image_anchor": 0, + "image_scale": 1, + "origin_center": false, + "bed_width": 300, + "bed_depth": 300, + "build_height": 300 + } +} diff --git a/src2/kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json b/src2/kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json new file mode 100644 index 00000000..e887f9ce --- /dev/null +++ b/src2/kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json @@ -0,0 +1,368 @@ +{ + "deviceName": "IMADE3D JellyBOX 1 N0.4", + "bedWidth": 177, + "bedDepth": 155, + "maxHeight": 140, + "mode": "FDM", + "new": false, + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodeLayer": [ + ";; --- layer {layer} {height} @ {z} ---", + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodePre": [ + ";============================", + "; Jellybox Start Script Begin ", + ";============================", + "; for slicer: Kiri:Moto", + "; modified 2024-02-01. by LG", + ";", + "G21 ;metric values", + "G90 ;absolute positioning", + "M107 ;start with the fan off", + ";", + "; === Homing and bed leveling (if necessary)", + ";", + "G28 ;home all axes", + "G29 O ;auto bed leveling procedure only if leveling NOT active", + ";M500 ;optionally save the mesh", + "G28 X ;home x to get as far from the plate as possible", + "M420 S1 ;(re) enable bed leveling if turned off by the G28", + "G0 Y0 F5000 ;position Y in front", + "G0 Z5 F3000 ;position Z", + "M190 S{bed_temp} T{tool} ;wait for the bed to reach desired temperature", + "M109 S{temp} T{tool} ;wait for the extruder to reach desired temperature", + "; ", + "; === Purge 30mm of the filament", + ";", + "G90 ;absolute positioning", + "M400 ;make sure all is finished", + "G92 E0 ;to reset extruder position to 0 after finished previous print", + "G1 E30 F200 ;extrude 30mm of feed stock", + "G92 E0 ;reset the extruder position Z is 0 again.", + ";", + ";===================================", + "; Jellybox Printer Start Script End", + ";===================================" + ], + "gcodePost": [ + ";---------------------------------", + ";;; Jellybox End Script Begin ;;;", + ";_________________________________", + "; end gcode last modified 2022-01-13", + "M117 Finishing Up ;write Finishing Up", + "M107 ;turn the fan off", + "M104 S0 ;extruder heater off", + "M140 S0 ;bed heater off (if you have it)", + "G91 ;relative positioning (includes extruder)", + "G1 E-1 F2500 ;retract the filament a bit before lifting the nozzle to release some of the pressure", + "G1 Z0.5 E-4 X-10 F9000 ;get out and retract filament even more", + "G1 E-25 F2000 ;retract even more", + "G90 ;absolute positioning (includes extruder)", + "G28 X ;home X so the head is out of the way", + "G1 Y140 ;move Y forward, so the print is more accessible", + "M84 ;steppers off", + "M117 Print finished ;write Print finished", + ";---------------------------------------", + ";;; Jellybox End Script End ;;;", + ";_______________________________________" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "fwRetract": false, + "profiles": [ + { + "processName": "JB1 PLA - N0.4 L0.3 T210", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 215, + "firstLayerBedTemp": 60, + "firstLayerRate": 20, + "firstLayerFillRate": 25, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 210, + "outputBedTemp": 60, + "outputFanSpeed": 130, + "outputFeedrate": 45, + "outputFinishrate": 20, + "outputSeekrate": 80, + "outputShellMult": 1.05, + "outputFillMult": 1.05, + "outputSparseMult": 1.2, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 1.8, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 0.2, + "antiBacklash": 0, + "sliceLineWidth": 0.45, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.2, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + }, + { + "processName": "JB1 FLEX - N0.4 L0.3 T235", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 238, + "firstLayerBedTemp": 55, + "firstLayerRate": 10, + "firstLayerFillRate": 10, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 235, + "outputBedTemp": 55, + "outputFanSpeed": 75, + "outputFeedrate": 15, + "outputFinishrate": 10, + "outputSeekrate": 80, + "outputShellMult": 1.2, + "outputFillMult": 1.2, + "outputSparseMult": 1.4, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 0.5, + "outputRetractSpeed": 20, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 1, + "antiBacklash": 0, + "sliceLineWidth": 0.45, + "sliceFillRepeat": 1, + "sliceFillRate": 15, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.2, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 10, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + }, + { + "processName": "JB1 PETG - N0.4 L0.3 T235", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 235, + "firstLayerBedTemp": 70, + "firstLayerRate": 20, + "firstLayerFillRate": 25, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 235, + "outputBedTemp": 70, + "outputFanSpeed": 75, + "outputFeedrate": 45, + "outputFinishrate": 20, + "outputSeekrate": 80, + "outputShellMult": 1.05, + "outputFillMult": 1.05, + "outputSparseMult": 1.2, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 2, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 0.2, + "antiBacklash": 0, + "sliceLineWidth": 0.45, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.2, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + } + ], + "filamentSource": "direct", + "gcodeTime": 1, + "gcodeFeature": [], + "extras": { + "palette": { + "printer": "9c94bc185556", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + } +} \ No newline at end of file diff --git a/src2/kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json b/src2/kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json new file mode 100644 index 00000000..66d22b41 --- /dev/null +++ b/src2/kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json @@ -0,0 +1,368 @@ +{ + "deviceName": "IMADE3D JellyBOX 2 N0.4", + "bedWidth": 177, + "bedDepth": 155, + "maxHeight": 140, + "mode": "FDM", + "new": false, + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodeLayer": [ + ";; --- layer {layer} {height} @ {z} ---", + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodePre": [ + ";============================", + "; Jellybox Start Script Begin ", + ";============================", + "; for slicer: Kiri:Moto", + "; modified 2024-02-01. by LG", + ";", + "G21 ;metric values", + "G90 ;absolute positioning", + "M107 ;start with the fan off", + ";", + "; === Homing and bed leveling (if necessary)", + ";", + "G28 ;home all axes", + "G29 O ;auto bed leveling procedure only if leveling NOT active", + ";M500 ;optionally save the mesh", + "G28 X ;home x to get as far from the plate as possible", + "M420 S1 ;(re) enable bed leveling if turned off by the G28", + "G0 Y0 F5000 ;position Y in front", + "G0 Z5 F3000 ;position Z", + "M190 S{bed_temp} T{tool} ;wait for the bed to reach desired temperature", + "M109 S{temp} T{tool} ;wait for the extruder to reach desired temperature", + "; ", + "; === Purge 30mm of the filament", + ";", + "G90 ;absolute positioning", + "M400 ;make sure all is finished", + "G92 E0 ;to reset extruder position to 0 after finished previous print", + "G1 E30 F200 ;extrude 30mm of feed stock", + "G92 E0 ;reset the extruder position Z is 0 again.", + ";", + ";===================================", + "; Jellybox Printer Start Script End", + ";===================================" + ], + "gcodePost": [ + ";---------------------------------", + ";;; Jellybox End Script Begin ;;;", + ";_________________________________", + "; end gcode last modified 2022-01-13", + "M117 Finishing Up ;write Finishing Up", + "M107 ;turn the fan off", + "M104 S0 ;extruder heater off", + "M140 S0 ;bed heater off (if you have it)", + "G91 ;relative positioning (includes extruder)", + "G1 E-1 F2500 ;retract the filament a bit before lifting the nozzle to release some of the pressure", + "G1 Z0.5 E-4 X-10 F9000 ;get out and retract filament even more", + "G1 E-25 F2000 ;retract even more", + "G90 ;absolute positioning (includes extruder)", + "G28 X ;home X so the head is out of the way", + "G1 Y140 ;move Y forward, so the print is more accessible", + "M84 ;steppers off", + "M117 Print finished ;write Print finished", + ";---------------------------------------", + ";;; Jellybox End Script End ;;;", + ";_______________________________________" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "fwRetract": false, + "profiles": [ + { + "processName": "JB2 PLA - N0.4 L0.3 T210", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 215, + "firstLayerBedTemp": 60, + "firstLayerRate": 20, + "firstLayerFillRate": 25, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 210, + "outputBedTemp": 60, + "outputFanSpeed": 130, + "outputFeedrate": 45, + "outputFinishrate": 20, + "outputSeekrate": 80, + "outputShellMult": 1.05, + "outputFillMult": 1.05, + "outputSparseMult": 1.2, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 1.8, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 0.2, + "antiBacklash": 0, + "sliceLineWidth": 0.45, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.2, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + }, + { + "processName": "JB2 PETG - N0.4 L0.3 T235", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 235, + "firstLayerBedTemp": 70, + "firstLayerRate": 20, + "firstLayerFillRate": 25, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 235, + "outputBedTemp": 70, + "outputFanSpeed": 75, + "outputFeedrate": 45, + "outputFinishrate": 20, + "outputSeekrate": 80, + "outputShellMult": 1.05, + "outputFillMult": 1.05, + "outputSparseMult": 1.2, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 2, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 0.2, + "antiBacklash": 0, + "sliceLineWidth": 0.45, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.2, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + }, + { + "processName": "JB2 FLEX - N0.4 L0.3 T235", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 238, + "firstLayerBedTemp": 55, + "firstLayerRate": 10, + "firstLayerFillRate": 10, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 235, + "outputBedTemp": 55, + "outputFanSpeed": 75, + "outputFeedrate": 15, + "outputFinishrate": 10, + "outputSeekrate": 80, + "outputShellMult": 1.2, + "outputFillMult": 1.2, + "outputSparseMult": 1.4, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 0.5, + "outputRetractSpeed": 20, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 1, + "antiBacklash": 0, + "sliceLineWidth": 0.45, + "sliceFillRepeat": 1, + "sliceFillRate": 15, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.2, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 10, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + } + ], + "filamentSource": "direct", + "gcodeTime": 1, + "gcodeFeature": [], + "extras": { + "palette": { + "printer": "e888752465a2", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + } +} \ No newline at end of file diff --git a/src2/kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json b/src2/kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json new file mode 100644 index 00000000..c5373457 --- /dev/null +++ b/src2/kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json @@ -0,0 +1,369 @@ +{ + "deviceName": "IMADE3D JellyBOX 3 N0.4", + "bedWidth": 180, + "bedDepth": 165, + "maxHeight": 140, + "mode": "FDM", + "new": false, + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodeLayer": [ + ";; --- layer {layer} {height} @ {z} ---", + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodePre": [ + ";============================", + "; Jellybox Start Script Begin ", + ";============================", + "; for slicer: Kiri:Moto", + "; modified 2024-02-01. by LG", + ";", + "G21 ;metric values", + "G90 ;absolute positioning", + "M107 ;start with the fan off", + ";", + "; === Homing and bed leveling (if necessary)", + ";", + "G28 ;home all axes", + "G29 O ;auto bed leveling procedure only if leveling NOT active", + ";M500 ;optionally save the mesh", + "G28 X ;home x to get as far from the plate as possible", + "M420 S1 ;(re) enable bed leveling if turned off by the G28", + "G0 Y0 F5000 ;position Y in front", + "G0 Z5 F3000 ;position Z", + "M190 S{bed_temp} T{tool} ;wait for the bed to reach desired temperature", + "M109 S{temp} T{tool} ;wait for the extruder to reach desired temperature", + "; ", + "; === Purge 30mm of the filament", + ";", + "G90 ;absolute positioning", + "M400 ;make sure all is finished", + "G92 E0 ;to reset extruder position to 0 after finished previous print", + "G1 E30 F200 ;extrude 30mm of feed stock", + "G92 E0 ;reset the extruder position Z is 0 again.", + ";", + ";===================================", + "; Jellybox Printer Start Script End", + ";===================================" + ], + "gcodePost": [ + ";============================", + "; Jellybox End Script Begin ", + ";============================", + "; modified 2024-02-01", + "M117 Finishing Up ;write Finishing Up", + "M107 ;turn the fan off", + "M104 S0 ;extruder heater off", + "M140 S0 ;bed heater off (if you have it)", + "G91 ;relative positioning (includes extruder)", + "G1 E-1 F2500 ;retract the filament a bit before lifting the nozzle to release some of the pressure", + "G1 Z0.5 E-4 X-10 F9000 ;get out and retract filament even more", + "G1 E-25 F2000 ;retract even more", + "G90 ;absolute positioning (includes extruder)", + "G28 X ;home X so the head is out of the way", + "G1 Y140 ;move Y forward, so the print is more accessible", + "G92 E0 ;reset the extruder position Z to 0", + "M84 ;steppers off", + "M117 Print finished ;write Print finished", + ";=========================", + "; Jellybox End Script End ", + ";=========================" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "fwRetract": false, + "profiles": [ + { + "processName": "JB3 PLA - N0.4 L0.3 T210", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 215, + "firstLayerBedTemp": 60, + "firstLayerRate": 20, + "firstLayerFillRate": 25, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 210, + "outputBedTemp": 60, + "outputFanSpeed": 130, + "outputFeedrate": 45, + "outputFinishrate": 20, + "outputSeekrate": 80, + "outputShellMult": 1.05, + "outputFillMult": 1.05, + "outputSparseMult": 1.2, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 1.8, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 0.2, + "antiBacklash": 0, + "sliceLineWidth": 0.45, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.2, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + }, + { + "processName": "JB3 PETG - N0.4 L0.3 T235", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 235, + "firstLayerBedTemp": 70, + "firstLayerRate": 20, + "firstLayerFillRate": 25, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 235, + "outputBedTemp": 70, + "outputFanSpeed": 75, + "outputFeedrate": 45, + "outputFinishrate": 20, + "outputSeekrate": 80, + "outputShellMult": 1.05, + "outputFillMult": 1.05, + "outputSparseMult": 1.2, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 2, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 0.2, + "antiBacklash": 0, + "sliceLineWidth": 0.45, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.2, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + }, + { + "processName": "JB3 TPU - N0.4 L0.3 T235", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 238, + "firstLayerBedTemp": 55, + "firstLayerRate": 10, + "firstLayerFillRate": 10, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 235, + "outputBedTemp": 55, + "outputFanSpeed": 75, + "outputFeedrate": 15, + "outputFinishrate": 10, + "outputSeekrate": 80, + "outputShellMult": 1.2, + "outputFillMult": 1.2, + "outputSparseMult": 1.4, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 0.5, + "outputRetractSpeed": 20, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 1, + "antiBacklash": 0, + "sliceLineWidth": 0.45, + "sliceFillRepeat": 1, + "sliceFillRate": 15, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.2, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 10, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + } + ], + "filamentSource": "direct", + "gcodeTime": 1, + "gcodeFeature": [], + "extras": { + "palette": { + "printer": "e292e5c84d0d", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + } +} \ No newline at end of file diff --git a/src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json b/src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json new file mode 100644 index 00000000..eec8ee1f --- /dev/null +++ b/src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json @@ -0,0 +1,369 @@ +{ + "deviceName": "IMADE3D JellyBOX 4 N0.4", + "bedWidth": 180, + "bedDepth": 165, + "maxHeight": 150, + "mode": "FDM", + "new": false, + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodeLayer": [ + ";; --- layer {layer} {height} @ {z} ---", + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodePre": [ + ";============================", + "; Jellybox Start Script Begin ", + ";============================", + "; for slicer: Kiri:Moto", + "; modified 2024-02-01. by LG", + ";", + "G21 ;metric values", + "G90 ;absolute positioning", + "M107 ;start with the fan off", + ";", + "; === Homing and bed leveling (if necessary)", + ";", + "G28 ;home all axes", + "G29 O ;auto bed leveling procedure only if leveling NOT active", + ";M500 ;optionally save the mesh", + "G28 X ;home x to get as far from the plate as possible", + "M420 S1 ;(re) enable bed leveling if turned off by the G28", + "G0 Y0 F5000 ;position Y in front", + "G0 Z5 F3000 ;position Z", + "M190 S{bed_temp} T{tool} ;wait for the bed to reach desired temperature", + "M109 S{temp} T{tool} ;wait for the extruder to reach desired temperature", + "; ", + "; === Purge 30mm of the filament", + ";", + "G90 ;absolute positioning", + "M400 ;make sure all is finished", + "G92 E0 ;to reset extruder position to 0 after finished previous print", + "G1 E30 F200 ;extrude 30mm of feed stock", + "G92 E0 ;reset the extruder position Z is 0 again.", + ";", + ";===================================", + "; Jellybox Printer Start Script End", + ";===================================" + ], + "gcodePost": [ + ";============================", + "; Jellybox End Script Begin ", + ";============================", + "; modified 2024-02-01", + "M117 Finishing Up ;write Finishing Up", + "M107 ;turn the fan off", + "M104 S0 ;extruder heater off", + "M140 S0 ;bed heater off (if you have it)", + "G91 ;relative positioning (includes extruder)", + "G1 E-1 F2500 ;retract the filament a bit before lifting the nozzle to release some of the pressure", + "G1 Z0.5 E-4 X-10 F9000 ;get out and retract filament even more", + "G1 E-25 F2000 ;retract even more", + "G90 ;absolute positioning (includes extruder)", + "G28 X ;home X so the head is out of the way", + "G1 Y140 ;move Y forward, so the print is more accessible", + "G92 E0 ;reset the extruder position Z to 0", + "M84 ;steppers off", + "M117 Print finished ;write Print finished", + ";=========================", + "; Jellybox End Script End ", + ";=========================" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "fwRetract": false, + "profiles": [ + { + "processName": "JB4 PLA - N0.4 L0.3 T215", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 215, + "firstLayerBedTemp": 60, + "firstLayerRate": 40, + "firstLayerFillRate": 30, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 215, + "outputBedTemp": 60, + "outputFanSpeed": 200, + "outputFeedrate": 70, + "outputFinishrate": 35, + "outputSeekrate": 120, + "outputShellMult": 1.15, + "outputFillMult": 1.15, + "outputSparseMult": 1.3, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 1.8, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 0.3, + "antiBacklash": 0, + "sliceLineWidth": 0.4, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": false, + "firstSliceHeight": 0.3, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + }, + { + "processName": "JB4 PETG - N0.4 L0.3 T240", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 235, + "firstLayerBedTemp": 70, + "firstLayerRate": 40, + "firstLayerFillRate": 50, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 240, + "outputBedTemp": 70, + "outputFanSpeed": 75, + "outputFeedrate": 70, + "outputFinishrate": 35, + "outputSeekrate": 100, + "outputShellMult": 1.05, + "outputFillMult": 1.05, + "outputSparseMult": 1.2, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 2, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 0.3, + "antiBacklash": 0, + "sliceLineWidth": 0.4, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.3, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + }, + { + "processName": "JB4 TPU - N0.4 L0.3 T230", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 230, + "firstLayerBedTemp": 55, + "firstLayerRate": 10, + "firstLayerFillRate": 10, + "firstLayerPrintMult": 1.2, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 230, + "outputBedTemp": 55, + "outputFanSpeed": 75, + "outputFeedrate": 23, + "outputFinishrate": 15, + "outputSeekrate": 100, + "outputShellMult": 1.15, + "outputFillMult": 1.15, + "outputSparseMult": 1.3, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 0.5, + "outputRetractSpeed": 20, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": true, + "zHopDistance": 1, + "antiBacklash": 0, + "sliceLineWidth": 0.4, + "sliceFillRepeat": 1, + "sliceFillRate": 15, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.3, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 10, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + } + ], + "filamentSource": "direct", + "gcodeTime": 1, + "gcodeFeature": [], + "extras": { + "palette": { + "printer": "57c7e3bd6e44", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + } +} \ No newline at end of file diff --git a/src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json b/src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json new file mode 100644 index 00000000..dbb8f6c2 --- /dev/null +++ b/src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json @@ -0,0 +1,374 @@ +{ + "deviceName": "IMADE3D JellyBOX 4 N0.6", + "bedWidth": 180, + "bedDepth": 165, + "maxHeight": 150, + "mode": "FDM", + "new": false, + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodeLayer": [ + ";; --- layer {layer} {height} @ {z} ---", + "M117 {progress}% layer {layer} / {layers}" + ], + "gcodePre": [ + ";============================", + "; Jellybox Start Script Begin ", + ";============================", + "; for slicer: Kiri:Moto", + "; modified 2024-02-01. by LG", + ";", + "G21 ;metric values", + "G90 ;absolute positioning", + "M107 ;start with the fan off", + ";", + "; === Homing and bed leveling (if necessary)", + ";", + "G28 ;home all axes", + "G29 O ;auto bed leveling procedure only if leveling NOT active", + ";M500 ;optionally save the mesh", + "G28 X ;home x to get as far from the plate as possible", + "M420 S1 ;(re) enable bed leveling if turned off by the G28", + "G0 Y0 F5000 ;position Y in front", + "G0 Z5 F3000 ;position Z", + "M190 S{bed_temp} T{tool} ;wait for the bed to reach desired temperature", + "M109 S{temp} T{tool} ;wait for the extruder to reach desired temperature", + "; ", + "; === Purge 30mm of the filament", + ";", + "G90 ;absolute positioning", + "M400 ;make sure all is finished", + "G92 E0 ;to reset extruder position to 0 after finished previous print", + "G1 E30 F200 ;extrude 30mm of feed stock", + "G92 E0 ;reset the extruder position Z is 0 again.", + ";", + "; === Print front line", + ";", + ";G0 Z0.2 ; move Z 0.3mm above bed", + ";G1 X160 E30 F1000 ; intro line ", + ";G92 E0 ;reset the extruder position Z is 0 again.", + ";===================================", + "; Jellybox Printer Start Script End", + ";===================================" + ], + "gcodePost": [ + ";============================", + "; Jellybox End Script Begin ", + ";============================", + "; modified 2024-02-01", + "M117 Finishing Up ;write Finishing Up", + "M107 ;turn the fan off", + "M104 S0 ;extruder heater off", + "M140 S0 ;bed heater off (if you have it)", + "G91 ;relative positioning (includes extruder)", + "G1 E-1 F2500 ;retract the filament a bit before lifting the nozzle to release some of the pressure", + "G1 Z0.5 E-4 X-10 F9000 ;get out and retract filament even more", + "G1 E-25 F2000 ;retract even more", + "G90 ;absolute positioning (includes extruder)", + "G28 X ;home X so the head is out of the way", + "G1 Y140 ;move Y forward, so the print is more accessible", + "G92 E0 ;reset the extruder position Z to 0", + "M84 ;steppers off", + "M117 Print finished ;write Print finished", + ";=========================", + "; Jellybox End Script End ", + ";=========================" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.6, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "fwRetract": false, + "profiles": [ + { + "processName": "JB4 PLA - N0.6 L0.3 T220", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0.1, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 220, + "firstLayerBedTemp": 65, + "firstLayerRate": 40, + "firstLayerFillRate": 50, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 220, + "outputBedTemp": 60, + "outputFanSpeed": 200, + "outputFeedrate": 60, + "outputFinishrate": 30, + "outputSeekrate": 100, + "outputShellMult": 1.1, + "outputFillMult": 1.1, + "outputSparseMult": 1.2, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 2.4, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": false, + "zHopDistance": 0.3, + "antiBacklash": 0, + "sliceLineWidth": 0.6, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.3, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + }, + { + "processName": "JB4 PETG - N0.6 L0.3 T245", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0.1, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 245, + "firstLayerBedTemp": 75, + "firstLayerRate": 30, + "firstLayerFillRate": 35, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 245, + "outputBedTemp": 70, + "outputFanSpeed": 128, + "outputFeedrate": 100, + "outputFinishrate": 50, + "outputSeekrate": 120, + "outputShellMult": 1.1, + "outputFillMult": 1.1, + "outputSparseMult": 1.2, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 2.8, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": false, + "zHopDistance": 0.3, + "antiBacklash": 0, + "sliceLineWidth": 0.6, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.3, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + }, + { + "processName": "JB4 TPU - N0.6 L0.3 T235", + "sliceHeight": 0.3, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0.1, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.6, + "sliceSupportGap": 1, + "sliceSupportSize": 6, + "sliceSupportArea": 3, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerNozzleTemp": 235, + "firstLayerBedTemp": 55, + "firstLayerRate": 10, + "firstLayerFillRate": 15, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1, + "firstLayerFanSpeed": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputTemp": 235, + "outputBedTemp": 40, + "outputFanSpeed": 64, + "outputFeedrate": 15, + "outputFinishrate": 10, + "outputSeekrate": 40, + "outputShellMult": 1.3, + "outputFillMult": 1.1, + "outputSparseMult": 1.3, + "outputBrimCount": 5, + "outputBrimOffset": 5, + "outputRetractDist": 2.8, + "outputRetractSpeed": 40, + "outputRetractWipe": 0.8, + "outputRetractDwell": 10, + "outputShortPoly": 50, + "outputMinSpeed": 10, + "outputCoastDist": 0.4, + "outputLayerRetract": false, + "zHopDistance": 1, + "antiBacklash": 0, + "sliceLineWidth": 0.6, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSupportSpan": 5, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "firstSliceHeight": 0.3, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFlatten": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0.15, + "ranges": [], + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputAlternating": false + } + ], + "filamentSource": "direct", + "gcodeTime": 1, + "gcodeFeature": [], + "extras": { + "palette": { + "printer": "3c0a256d3a4a", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + } +} \ No newline at end of file diff --git a/src2/kiri-dev/fdm/Leapfrog.CreatrHS.json b/src2/kiri-dev/fdm/Leapfrog.CreatrHS.json new file mode 100644 index 00000000..b61f006d --- /dev/null +++ b/src2/kiri-dev/fdm/Leapfrog.CreatrHS.json @@ -0,0 +1,35 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z axis", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G1 E15 F300 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G1 Z{z_max} F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 270, + "bed_depth": 280, + "build_height": 180 + } +} diff --git a/src2/kiri-dev/fdm/MakerGear.M2.json b/src2/kiri-dev/fdm/MakerGear.M2.json new file mode 100644 index 00000000..250a4937 --- /dev/null +++ b/src2/kiri-dev/fdm/MakerGear.M2.json @@ -0,0 +1,37 @@ +{ + "pre":[ + "M108 S255 ; set extruder speed", + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 XY ; home XY axes", + "G28 Z ; home Z axis", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G1 E15 F300 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M108 S0 ; set extruder speed", + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G1 Z200 F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 200, + "bed_depth": 250, + "build_height": 200 + } +} diff --git a/src2/kiri-dev/fdm/Makerbot.Replicator2.json b/src2/kiri-dev/fdm/Makerbot.Replicator2.json new file mode 100644 index 00000000..29070bcb --- /dev/null +++ b/src2/kiri-dev/fdm/Makerbot.Replicator2.json @@ -0,0 +1,44 @@ +{ + "pre":[ + "M73 P0 ; set progress to 0%", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M104 S{temp} T{tool} ; set extruder temperature for T{tool} (tool 0)", + "M127 ; close extruder valve (if it has one)", + "G162 X Y F3000 ; move X,Y axis to maximum position at speed 3000 (top/right of table)", + "G161 Z F1200 ; move Z axis to minimum position as speed 1200 (bottom)", + "G92 Z-5 ; set Z axis value to -5 (no move)", + "G1 Z0 ; move Z axis to 0 (lifting it 5mm off endstop)", + "G161 Z F100 ; move Z axis to minimum position at speed 100", + "M132 X Y Z A B ; load EEPROM home offsets for all axis", + "G1 X{left} Y{bottom+10} Z30 F9000 ; move to wait position off table", + "G130 X20 Y20 Z20 A20 B20 ; lower stepper Vrefs to give more juice to the extruder heater", + "M133 T{tool} ; wait for extruder T{tool} temperature to stabilize", + "G130 X127 Y127 Z40 A127 B127 ; default stepper Vrefs now that extruder is hot", + "G92 A0 ; set extruder position value to 0 (no move)", + "G1 Z0.4 ; move nozzle just above the bed height", + "G1 E25 F300 ; purge extruder for 25mm at speed 300", + "G1 X{left+10} Y{bottom+10} Z0.15 F1200 ; slow wipe", + "G1 X{left+15} Y{bottom+15} Z0.5 F1200 ; lift", + "G92 A0 ; set extruder position value to 0 (no move)", + "M135 T{tool} ; set toolhead 0" + ], + "post":[ + "M127 ; stop filament cooling fan", + "M73 P100 ; set build progress to 100%", + "G1 Z150 F1000 ; send Z axis to bottom (engaging end stop)", + "G162 X Y F3000 ; move X,Y axis to maximum position at speed 3000 (top/right of table)", + "M18 ; disable all stepper motors", + "M72 P1 ; play song 1 (tada)" + ], + "cmd":{ + "fan_power": "M126 S{fan_speed}", + "progress": "M73 P{progress}" + }, + "settings":{ + "origin_center": true, + "bed_width": 300, + "bed_depth": 175, + "build_height": 150 + } +} diff --git a/src2/kiri-dev/fdm/My.Bambu.P1S b/src2/kiri-dev/fdm/My.Bambu.P1S new file mode 100644 index 00000000..4aee21dd --- /dev/null +++ b/src2/kiri-dev/fdm/My.Bambu.P1S @@ -0,0 +1,535 @@ +{ + "mode": "FDM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 250, + "bedDepth": 250, + "bedRound": false, + "maxHeight": 250, + "originCenter": false, + "gcodeFan": [ + "M106 P1 S{fan_speed}" + ], + "gcodeTrack": [ + "M73 P{progress}" + ], + "gcodeLayer": [ + "; CHANGE_LAYER", + "; layer num/total_layer_count: {layer}/{layers}", + "M622.1 S1 ; for prev firware, default turned on", + "M1002 judge_flag timelapse_record_flag", + "M622 J1", + "M971 S11 C10 O0 ; timelapse without wipe tower", + "M623", + "M73 L{layer}", + "M991 S0 P{layer-1} ; notify layer change", + ";; IF { layer == 0 }", + "M204 S500", + ";; ELSE", + "M204 S5000", + ";; END" + ], + "gcodePre": [ + ";; DEFINE BAMBU-AMS 2,0,1", + "; ===== HEADER =====================================================================", + "M73 P0 R14 ; Initial progress and time remaining", + "M201 X20000 Y20000 Z500 E5000 ; Set max acceleration", + "M203 X500 Y500 Z20 E30 ; Set max feedrates", + "M204 P20000 R5000 T20000 ; Set acceleration", + "M205 X9.00 Y9.00 Z3.00 E2.50 ; Set advanced settings", + "M106 S0 ; Set part fan speed to 0", + "M106 P2 S0 ; Turn off aux fan", + ";===== machine: P1S ========================", + "M104 S75 ; Set extruder temp to turn on HB fan, prevent filament oozing", + "M710 A1 S255 ; Turn on MC fan by default (P1S)", + ";===== Reset machine status =================", + "M290 X40 Y40 Z2.6666666 ; Set baby stepping offsets", + "G91 ; Set to relative positioning", + "M17 Z0.4 ; Lower Z-motor current", + "G380 S2 Z30 F300 ; Lower hotbed (G380 like G38)", + "G380 S2 Z-25 F300 ; Continue to lower bed", + "G1 Z5 F300 ; Raise bed by 5mm", + "G90 ; Set to absolute positioning", + "M17 X1.2 Y1.2 Z0.75 ; Reset motor current to default", + "M960 S5 P1 ; Turn on logo lamp", + "M220 S100 ; Reset feedrate to 100%", + "M221 S100 ; Reset flowrate to 100%", + "M73.2 R1.0 ; Reset remaining time magnitude", + "M1002 set_gcode_claim_speed_level : 5 ; Set G-code claim speed level", + "M221 X0 Y0 Z0 ; Turn off soft endstop to prevent logic issues", + "G29.1 Z0 ; Clear Z-trim value", + "M204 S10000 ; Initialize acceleration to 10 m/s^2", + ";===== Heatbed preheat ====================", + "M1002 gcode_claim_action : 2 ; Claim action for preheating", + "M140 S{bed_temp} ; Set bed temperature to 65°C", + "M190 S{bed_temp} ; Wait for bed temperature to reach 65°C", + ";===== Turn on fans to prevent PLA jamming =====", + "M106 P3 S180 ; set chamber fan speed to prevent PLA jamming", + "M106 P2 S100 ; Turn on aux fan to cool toolhead", + ";===== Prepare print temperature and material =====", + "M104 S{temp} ; Set extruder temp to 220°C", + "G91 ; Set to relative positioning", + "G0 Z10 F1200 ; Raise Z by 10mm", + "G90 ; Set to absolute positioning", + "G28 X ; Home X axis", + "M975 S1 ; Turn on feature (guess: vibration suppression)", + "M73 P31 R9 ; Update progress and time remaining", + "G1 X60 F12000 ; Fast move to X60", + "G1 Y245 ; Move to Y245", + "G1 Y265 F3000 ; Move to Y265 slowly", + "M620 M ; Custom command (guess: material management)", + "M620 S{tool}A ; Switch material if AMS present", + "M109 S{temp} ; Wait for extruder to reach 220°C", + "G1 X120 F12000 ; Fast move to X120", + "G1 X20 Y50 F12000 ; Fast move to X20 Y50", + "G1 Y-3 ; Move down Y-axis by 3", + "T{tool} ; Select starting tool", + "G1 X54 F12000 ; Fast move to X54", + "G1 Y265 ; Move to Y265", + "M400 ; Wait for all moves to finish", + "M621 S{tool}A ; Custom command", + "M620.1 E F523.843 T240 ; Extrude command with custom parameters", + "M412 S1 ; Turn on filament runout detection", + "M109 S250 ; Set nozzle to common flush temp", + "M106 P1 S0 ; Turn off nozzle fan", + "G92 E0 ; Reset extruder position", + "G1 E50 F200 ; Extrude 50mm of filament", + "M400 ; Wait for moves to finish", + "M104 S{temp} ; Set nozzle temperature to 220°C", + "G92 E0 ; Reset extruder position", + "G1 E50 F200 ; Extrude 50mm of filament", + "M400 ; Wait for moves to finish", + "M106 P1 S255 ; Turn on nozzle fan to full speed", + "G92 E0 ; Reset extruder position", + "G1 E5 F300 ; Extrude 5mm of filament", + "M109 S200 ; Drop nozzle temperature to 200°C to shrink filament", + "G92 E0 ; Reset extruder position", + "M73 P33 R9 ; Update progress and time remaining", + "G1 E-0.5 F300 ; Retract filament by 0.5mm", + "M73 P35 R9 ; Update progress and time remaining", + "G1 X70 F9000 ; Fast move to X70", + "G1 X76 F15000 ; Fast move to X76", + "G1 X65 F15000 ; Fast move to X65", + "G1 X76 F15000 ; Repeat movements to shake and clean nozzle", + "G1 X65 F15000 ; Shake to put down garbage", + "G1 X80 F6000 ; Move to X80", + "G1 X95 F15000 ; Fast move to X95", + "G1 X80 F15000 ; Return to X80", + "G1 X165 F15000 ; Wipe and shake", + "M400 ; Wait for all moves to finish", + "M106 P1 S0 ; Turn off nozzle fan", + ";===== Prepare print temperature and material end =====", + ";===== Wipe nozzle ===============================", + "M1002 gcode_claim_action : 14 ; Claim action for nozzle wipe", + "M975 S1 ; Turn on feature (guess: vibration suppression)", + "M106 S255 ; Turn on part fan to full speed", + "G1 X65 Y230 F18000 ; Fast move to start of wipe", + "G1 Y264 F6000 ; Wipe along Y-axis", + "M109 S200 ; Set nozzle temperature to 200°C", + "G1 X100 F18000 ; Wipe first pass", + "G0 X135 Y253 F20000 ; Move to exposed steel surface edge", + "G28 Z P0 T300 ; Home Z with low precision, permit 300°C temperature", + "G29.2 S0 ; Turn off ABL (auto bed leveling)", + "G0 Z5 F20000 ; Raise Z by 5mm", + "G1 X60 Y265 ; Move to X60 Y265", + "G92 E0 ; Reset extruder position", + "G1 E-0.5 F300 ; Retract more", + "G1 X100 F5000 ; Second wipe pass", + "G1 X70 F15000 ; Repeat movements for further wiping", + "G1 X100 F5000", + "G1 X70 F15000", + "G1 X100 F5000", + "G1 X70 F15000", + "G1 X90 F5000", + "G0 X128 Y261 Z-1.5 F20000 ; Move to exposed steel surface, stop nozzle", + "M104 S140 ; Set temp down to heatbed acceptable", + "M106 S255 ; Turn on part fan (G28 turned off fan)", + "M221 S ; Push soft endstop status", + "M221 Z0 ; Turn off Z axis endstop", + "G0 Z0.5 F20000 ; Raise Z by 0.5mm", + "G0 X125 Y259.5 Z-1.01 ; Move to wiping position", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y262.5", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y260.0", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y262.0", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y260.5", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y261.5", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y261.0", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 X128", + "G2 I0.5 J0 F300 ; Arc movements for final cleaning", + "M73 P36 R8 ; Update progress and time remaining", + "G2 I0.5 J0 F300", + "G2 I0.5 J0 F300", + "G2 I0.5 J0 F300", + "M109 S140 ; Wait for nozzle temp to reach heatbed acceptable temp", + "G2 I0.5 J0 F3000 ; More arc movements", + "G2 I0.5 J0 F3000", + "G2 I0.5 J0 F3000", + "G2 I0.5 J0 F3000", + "M221 R ; Pop softend status", + "G1 Z10 F1200 ; Raise Z by 10mm", + "M400 ; Wait for all moves to finish", + "G1 Z10 ; Raise Z by 10mm", + "G1 F30000 ; Set fast feedrate", + "G1 X230 Y15 ; Move to X230 Y15", + "G29.2 S1 ; Turn on ABL", + "M106 S0 ; Turn off part fan, too noisy", + ";===== Wipe nozzle end ================================", + ";===== Bed leveling ==================================", + "M1002 judge_flag g29_before_print_flag ; Judge flag before leveling", + "M622 J1", + " M1002 gcode_claim_action : 1 ; Claim action for bed leveling", + " G29 A X115.2 Y115.2 I25.6 J25.6 ; Perform bed leveling", + " M400 ; Wait for all moves to finish", + " M500 ; Save calibration data", + "M623", + ";===== Bed leveling end ================================", + ";===== Home after wipe mouth =========================", + "M1002 judge_flag g29_before_print_flag ; Judge flag before homing", + "M622 J0", + " M1002 gcode_claim_action : 13 ; Claim action for homing", + " G28 ; Home all axes", + "M623", + ";===== Home after wipe mouth end =======================", + "M975 S1 ; Turn on vibration suppression", + ";===== Turn on fans to prevent PLA jamming =====", + "M106 P3 S180 ; set chamber fan speed to Prevent PLA jamming", + "M106 P2 S100 ; Turn on aux fan to cool toolhead", + "M104 S{temp} ; Set extruder temp to 220°C earlier, reduce wait time", + ";===== Mech mode fast check ==========================", + "G1 X128 Y128 Z10 F20000 ; Move to center for check", + "M400 P200 ; Wait and perform check", + "M970.3 Q1 A7 B30 C80 H15 K0 ; Custom command (mechanical check)", + "M974 Q1 S2 P0 ; Custom command", + "G1 X128 Y128 Z10 F20000 ; Move again to center for check", + "M400 P200 ; Wait and perform another check", + "M970.3 Q0 A7 B30 C90 Q0 H15 K0 ; Custom command", + "M974 Q0 S2 P0 ; Custom command", + "M975 S1 ; Turn on feature (vibration suppression)", + "G1 F30000 ; Set fast feedrate", + "M73 P37 R8 ; Update progress and time remaining", + "G1 X230 Y15 ; Move to X230 Y15", + "G28 X ; Re-home X and Y", + ";===== Mech mode fast check end =========================", + ";===== Nozzle load line ===============================", + "M975 S1 ; Turn on vibration suppression", + "G90 ; Set to absolute positioning", + "M83 ; Set extruder to relative mode", + "T1000 ; Custom command", + "G1 X18.0 Y1.0 Z0.8 F18000 ; Move to start position", + "M109 S{temp} ; Set extruder temp to 220°C", + "G1 Z0.2 ; Lower Z to 0.2mm", + "G0 E2 F300 ; Extrude 2mm of filament", + "G0 X240 E25 F5539.96 ; Move to X240 while extruding", + "G0 Y15 E1.166 F1384.99 ; Move along Y while extruding", + "G0 X239.5 ; Move to X239.5", + "G0 E0.2 ; Extrude 0.2mm of filament", + "G0 Y1.5 E1.166 ; Move along Y while extruding", + "G0 X18 E25 F5539.96 ; Return to start position while extruding", + "M400 ; Wait for all moves to finish", + ";===== For Textured PEI Plate ========================", + ";G29.1 Z-0.04 ; Adjust Z-offset for textured PEI plate", + ";===== Turn off light and wait extrude temperature =====", + "M1002 gcode_claim_action : 0 ; Claim action", + "M106 S0 ; Turn off part fan", + "M106 P2 S0 ; Turn off aux fan", + "M106 P3 S0 ; Turn off chamber fan", + "M975 S1 ; Turn on mechanical mode suppression", + "G90 ; Set to absolute positioning", + "G21 ; Set units to millimeters", + "M83 ; Use relative distances for extrusion", + "M106 P3 S200 ; Set chamber fan speed to 200 (prevents PLA jamming)", + "M981 S1 P20000 ; Open spaghetti detector" + ], + "gcodePost": [ + "; ===== FOOTER =====================================================================", + "M106 S0 ; Turn off part cooling fan", + "M106 P2 S0 ; Turn off auxiliary cooling fan", + "M981 S0 P20000 ; Close spaghetti detector", + "M106 P3 S0 ; Turn off chamber fan", + "M400 ; Wait for buffer to clear", + "G92 E0 ; Zero the extruder", + "G1 E-0.8 F1800 ; Retract filament by 0.8mm", + "G91 ; relative moves", + "G1 Z5 F900 ; drop bed 5mm", + "G90 ; absolute moves", + "G1 X65 Y245 F12000 ; Move to safe position X65 Y245", + "G1 Y265 F3000 ; Move to Y265 slowly", + "G1 X65 Y245 F12000 ; Move back to X65 Y245", + "G1 Y265 F3000 ; Move to Y265 slowly again", + "M140 S0 ; Turn off heated bed", + "M106 S0 ; Turn off part cooling fan", + "M106 P2 S0 ; Turn off auxiliary cooling fan", + "M106 P3 S0 ; Turn off chamber cooling fan", + "G1 X100 F12000 ; Wipe nozzle", + "M620 S255 ; Pull back filament to AMS", + "G1 X20 Y50 F12000 ; Move to X20 Y50", + "G1 Y-3 ; Move down Y-axis by 3", + "T255 ; Tool change command (educated guess)", + "G1 X65 F12000 ; Move to X65", + "G1 Y265 ; Move to Y265", + "G1 X100 F12000 ; Wipe nozzle again", + "M621 S255 ; Custom command (educated guess)", + "M104 S0 ; Turn off hotend", + "M622.1 S1 ; Enable feature (default for older firmware)", + "M1002 judge_flag timelapse_record_flag ; Check if timelapse recording is needed", + "M622 J1 ; Enable timelapse", + " M400 ; Wait for all motions to complete", + " M991 S0 P-1 ; End smooth timelapse at safe position", + " M400 S3 ; Wait for last picture to be taken", + "M623 ; End of \"timelapse_record_flag\"", + "M400 ; Wait for all motions to complete", + "M17 S ; Engage stepper motors", + "M17 Z0.4 ; Lower Z motor current to reduce impact if obstruction exists", + "G1 Z{z_max} F600 ; Move Z-axis to 125.5mm", + "M400 P100 ; Wait for 100ms", + "M17 R ; Restore Z motor current", + "M220 S100 ; Reset feedrate magnitude to 100%", + "M201.2 K1.0 ; Reset acceleration magnitude", + "M73.2 R1.0 ; Reset remaining time magnitude", + "M1002 set_gcode_claim_speed_level : 0 ; Set G-code claim speed level to 0", + "M17 X0.8 Y0.8 Z0.5 ; Lower motor current to 45% power", + "M73 P100 R0 ; Set progress to 100% with 0 remaining time" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.6, + "extSelect": [ + "; BEGIN tool change from {last_tool} to {tool}", + "M620 S{tool}A ; prepare tool change", + "M204 S9000 ; Set acceleration to 9000 mm/s^2", + "M620.11 S0 ; Reset filament retraction state", + "M400 ; wait for moves to complete", + "M620.1 E F187 T240 ; set purge rate 187 temp 240", + "T{tool} ; initiate tool change", + "M620.1 E F374 T240 ; set purge rate 374 temp 240", + "M620.11 S0 ; Reset filament retraction state", + "G92 E0 ; reset extruder position", + "M400 ; wait for moves to complete", + " ; -- PURGE --", + "M109 S240 ; Set nozzle temperature to 240°C", + "G1 E23.7 F187 ; Extrude 23.7mm of filament at flowrate 187", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F187 ; Continue extrusion at flowrate 187", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E-2 F1800 ; Retract filament by 2mm", + "G1 E2 F300 ; Recover retraction slowly", + "M400 ; Wait for all movements to complete", + "M109 S{temp} ; Set nozzle temperature back to print temp", + " ; -- SHAKE --", + "M106 P1 S255 ; Turn on nozzle cooling fan to full speed", + "M400 S3 ; Wait for all movements to complete", + "G1 X70 F5000 ; Move to X70", + "G1 X90 F3000 ; Move to X90", + "G1 Y255 F4000 ; Move to Y255", + "G1 X105 F5000 ; Move to X105", + "G1 Y265 ; Move to Y265", + "G1 X70 F10000 ; Rapid move to X70", + "G1 X100 F5000 ; Move to X100", + "G1 X70 F10000 ; Rapid move back to X70", + "G1 X100 F5000 ; Move to X100", + "G1 X70 F10000 ; Shake filament residue", + "G1 X80 F15000", + "G1 X60", + "G1 X80", + "G1 X60", + "G1 X80 ; Shake to put down garbage", + "G1 X100 F5000 ; Move to X100", + "G1 X165 F15000 ; Wipe and shake", + "G1 Y256 ; Move Y aside to prevent collision", + "M400 ; Wait for all movements to complete", + "M204 S10000 ; Set acceleration to 10000 mm/s^2", + "M621 S{tool}A ; complete tool change", + "; END tool change" + ], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + }, + { + "extFilament": 1.75, + "extNozzle": 0.6, + "extSelect": [], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + }, + { + "extFilament": 1.75, + "extNozzle": 0.6, + "extSelect": [], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + }, + { + "extFilament": 1.75, + "extNozzle": 0.6, + "extSelect": [], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + } + ], + "new": false, + "deviceName": "My Bambu P1S", + "bedBelt": false, + "fwRetract": false, + "filamentSource": "direct", + "extras": { + "bbl": { + "scrappy": { + "host": "10.10.2.5", + "code": "38373335", + "serial": "01S00A292500224" + }, + "luke": { + "host": "10.20.20.75", + "code": "20709223", + "serial": "01P09C482401767", + "modified": true + }, + "leia": { + "host": "10.20.20.76", + "code": "35129782", + "serial": "01P09C461603301", + "modified": true + }, + "vader": { + "host": "10.20.20.77", + "code": "17711811", + "serial": "01P00A413100516", + "modified": true + } + } + }, + "imageURL": "", + "deviceZMax": 0, + "gcodeTime": 1, + "gcodeFeature": [], + "profiles": [ + { + "processName": "Bambu Test", + "sliceHeight": 0.2, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.5, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 60, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 0, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 20, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 0, + "firstLayerFanSpeed": 0, + "outputTemp": 220, + "outputBedTemp": 65, + "outputFanSpeed": 255, + "outputFeedrate": 120, + "outputFinishrate": 90, + "outputSeekrate": 200, + "outputShellMult": 1.2, + "outputFillMult": 1.2, + "outputSparseMult": 1.2, + "outputRetractDist": 2, + "outputRetractSpeed": 80, + "outputRetractWipe": 0, + "outputRetractDwell": 0, + "outputShortPoly": 100, + "outputMinSpeed": 5, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.3, + "firstLayerFillRate": 80, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 210, + "firstLayerBedTemp": 60, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "sliceLineWidth": 0, + "sliceFillRepeat": 2, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "outputAlternating": false, + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputFanLayer": 1, + "outputNozzle": 0, + "sliceAngle": 45, + "sliceZInterleave": false + } + ] +} \ No newline at end of file diff --git a/src2/kiri-dev/fdm/Prusa.i3.MK2S.json b/src2/kiri-dev/fdm/Prusa.i3.MK2S.json new file mode 100644 index 00000000..25fff856 --- /dev/null +++ b/src2/kiri-dev/fdm/Prusa.i3.MK2S.json @@ -0,0 +1,317 @@ +{ + "deviceName": "Prusa i3 MK2S", + "new": false, + "mode": "FDM", + "internal": 0, + "bedWidth": 250, + "bedDepth": 210, + "maxHeight": 210, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "originCenter": false, + "extrudeAbs": false, + "spindleMax": 0, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M73 P{progress}" + ], + "gcodeLayer": [ + ";; LAYER {layer} of {layers}", + "M117 Layer {layer} of {layers}" + ], + "gcodePre": [ + "M107 ; disable cooling fan", + "M104 S160 ; set extruder temp", + "M140 S{bed_temp} ; set bed temp", + "M190 S{bed_temp} ; wait for bed temp", + "G28 W ; home all without mesh bed level", + "G80 ; mesh bed leveling", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "G1 F1000 ; set feedrate", + "G1 Y-3.0 Z0.5 ; park outside print area", + "M104 S{temp} ; set extruder actual required temp", + "M109 S{temp} ; wait for extruder actual required temp", + "G1 X60.0 E9.0 ; purge line", + "G1 X100.0 E12.5 ; purge line", + "M117 Printing" + ], + "gcodePost": [ + "G0 E-1 F900 ; retract slightly", + "M104 S0 ; turn off nozzle heat", + "M140 S0 ; turn off bed heat", + "M107 ; turn off fan", + "G0 F3000 ; set feedrate", + "G0 Y210 Z210 ; serve up print", + "G4 ; wait", + "M84 ; disable steppers", + "M117 Print Done" + ], + "gcodeProc": "", + "gcodeDwell": [], + "gcodeSpindle": [], + "gcodeChange": [], + "gcodeFExt": "gcode", + "gcodeSpace": true, + "gcodeStrip": false, + "gcodeLaserOn": [], + "gcodeLaserOff": [], + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.45, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "fwRetract": false, + "profiles": [ + { + "processName": "MK2S PLA", + "sliceHeight": 0.2, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "center", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceSolidLayers": 3, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 200, + "firstLayerFanSpeed": 0, + "outputTemp": 205, + "outputBedTemp": 70, + "outputFanSpeed": 255, + "outputFeedrate": 100, + "outputFinishrate": 30, + "outputSeekrate": 150, + "outputShellMult": 1, + "outputFillMult": 1.1, + "outputSparseMult": 1.1, + "outputRetractDist": 1, + "outputRetractSpeed": 80, + "outputRetractWipe": 2, + "outputRetractDwell": 0, + "outputShortPoly": 50, + "outputMinSpeed": 1, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0.12, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.2, + "firstLayerFillRate": 35, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "outputLoops": 0, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "sliceFillRepeat": 4, + "sliceLineWidth": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "beltAnchor": 200 + },{ + "processName": "MK2S PLA Fast", + "sliceHeight": 0.25, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "center", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceSolidLayers": 3, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 200, + "firstLayerFanSpeed": 0, + "outputTemp": 205, + "outputBedTemp": 70, + "outputFanSpeed": 255, + "outputFeedrate": 100, + "outputFinishrate": 40, + "outputSeekrate": 150, + "outputShellMult": 1.05, + "outputFillMult": 1.15, + "outputSparseMult": 1.15, + "outputRetractDist": 1, + "outputRetractSpeed": 80, + "outputRetractWipe": 2, + "outputRetractDwell": 0, + "outputShortPoly": 50, + "outputMinSpeed": 1, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0.12, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.25, + "firstLayerFillRate": 35, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "outputLoops": 0, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "sliceFillRepeat": 4, + "sliceLineWidth": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "beltAnchor": 200 + },{ + "processName": "MK2S PLA Fine", + "sliceHeight": 0.15, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "center", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.08, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 5, + "sliceSolidLayers": 4, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1.02, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 200, + "firstLayerFanSpeed": 0, + "outputTemp": 195, + "outputBedTemp": 70, + "outputFanSpeed": 255, + "outputFeedrate": 40, + "outputFinishrate": 25, + "outputSeekrate": 150, + "outputShellMult": 0.92, + "outputFillMult": 1.1, + "outputSparseMult": 1.1, + "outputRetractDist": 1, + "outputRetractSpeed": 80, + "outputRetractWipe": 2, + "outputRetractDwell": 0, + "outputShortPoly": 50, + "outputMinSpeed": 1, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0.12, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.2, + "firstLayerFillRate": 35, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "outputLoops": 0, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "sliceFillRepeat": 4, + "sliceLineWidth": 0.5, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "beltAnchor": 200 + } + ] +} diff --git a/src2/kiri-dev/fdm/Prusa.i3.MK3S+.json b/src2/kiri-dev/fdm/Prusa.i3.MK3S+.json new file mode 100644 index 00000000..45e2b92c --- /dev/null +++ b/src2/kiri-dev/fdm/Prusa.i3.MK3S+.json @@ -0,0 +1,325 @@ +{ + "deviceName": "Prusa i3 MK3S+", + "new": false, + "mode": "FDM", + "internal": 0, + "bedWidth": 250, + "bedDepth": 210, + "maxHeight": 210, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "originCenter": false, + "extrudeAbs": false, + "spindleMax": 0, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeTrack": [ + "M73 P{progress}" + ], + "gcodeLayer": [ + ";; LAYER {layer} of {layers}", + "M117 Layer {layer} of {layers}" + ], + "gcodePre": [ + "M201 X1000 Y1000 Z200 E5000 ; accel", + "M203 X200 Y200 Z12 E120 ; feedrate", + "M204 P1250 R1250 T1250 ; accel", + "M205 X8.00 Y8.00 Z0.40 E4.50 ; jerk", + "M205 S0 T0 ; min travel", + "M107 ; disable cooling fan", + "M862.3 P \"MK3S\" ; printer model check", + "M862.1 P0.4 ; nozzle diameter check", + "M104 S160 ; set extruder temp", + "M140 S{bed_temp} ; set bed temp", + "M190 S{bed_temp} ; wait for bed temp", + "G28 W ; home all without mesh bed level", + "G80 ; mesh bed leveling", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M900 K0.04 ; linear advance", + "G1 F1000 ; set feedrate", + "G1 Y-3.0 Z0.5 ; park outside print area", + "M104 S{temp} ; set extruder actual required temp", + "M109 S{temp} ; wait for extruder actual required temp", + "G1 X60.0 E9.0 ; purge line", + "G1 X100.0 E12.5 ; purge line", + "M117 Printing" + ], + "gcodePost": [ + "G0 E-1 F900 ; retract slightly", + "M104 S0 ; turn off nozzle heat", + "M140 S0 ; turn off bed heat", + "M107 ; turn off fan", + "G0 F3000 ; set feedrate", + "G0 Y210 Z210 ; serve up print", + "G4 ; wait", + "M84 ; disable steppers", + "M117 Print Done" + ], + "gcodeProc": "", + "gcodeDwell": [], + "gcodeSpindle": [], + "gcodeChange": [], + "gcodeFExt": "gcode", + "gcodeSpace": true, + "gcodeStrip": false, + "gcodeLaserOn": [], + "gcodeLaserOff": [], + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.45, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "fwRetract": false, + "profiles": [ + { + "processName": "MK3S+ PLA", + "sliceHeight": 0.2, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "center", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceSolidLayers": 3, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 200, + "firstLayerFanSpeed": 0, + "outputTemp": 205, + "outputBedTemp": 70, + "outputFanSpeed": 255, + "outputFeedrate": 100, + "outputFinishrate": 30, + "outputSeekrate": 150, + "outputShellMult": 1.05, + "outputFillMult": 1.1, + "outputSparseMult": 1.1, + "outputRetractDist": 1, + "outputRetractSpeed": 80, + "outputRetractWipe": 2, + "outputRetractDwell": 0, + "outputShortPoly": 50, + "outputMinSpeed": 1, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0.12, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.2, + "firstLayerFillRate": 35, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "outputLoops": 0, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "sliceFillRepeat": 4, + "sliceLineWidth": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "beltAnchor": 200 + },{ + "processName": "MK3S+ PLA Fast", + "sliceHeight": 0.25, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "center", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceSolidLayers": 3, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 200, + "firstLayerFanSpeed": 0, + "outputTemp": 205, + "outputBedTemp": 70, + "outputFanSpeed": 255, + "outputFeedrate": 100, + "outputFinishrate": 40, + "outputSeekrate": 150, + "outputShellMult": 1.1, + "outputFillMult": 1.15, + "outputSparseMult": 1.15, + "outputRetractDist": 1, + "outputRetractSpeed": 80, + "outputRetractWipe": 2, + "outputRetractDwell": 0, + "outputShortPoly": 50, + "outputMinSpeed": 1, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0.12, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.25, + "firstLayerFillRate": 35, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "outputLoops": 0, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "sliceFillRepeat": 4, + "sliceLineWidth": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "beltAnchor": 200 + },{ + "processName": "MK3S+ PLA Fine", + "sliceHeight": 0.15, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "center", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 5, + "sliceSolidLayers": 4, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1.02, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 200, + "firstLayerFanSpeed": 0, + "outputTemp": 195, + "outputBedTemp": 70, + "outputFanSpeed": 255, + "outputFeedrate": 40, + "outputFinishrate": 25, + "outputSeekrate": 150, + "outputShellMult": 0.92, + "outputFillMult": 1.1, + "outputSparseMult": 1.1, + "outputRetractDist": 1, + "outputRetractSpeed": 80, + "outputRetractWipe": 2, + "outputRetractDwell": 0, + "outputShortPoly": 50, + "outputMinSpeed": 1, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0.12, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.2, + "firstLayerFillRate": 35, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "outputLoops": 0, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "sliceFillRepeat": 4, + "sliceLineWidth": 0.5, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "beltAnchor": 200 + } + ] +} diff --git a/src2/kiri-dev/fdm/Prusa.mini.json b/src2/kiri-dev/fdm/Prusa.mini.json new file mode 100644 index 00000000..c85c7c47 --- /dev/null +++ b/src2/kiri-dev/fdm/Prusa.mini.json @@ -0,0 +1,164 @@ +{ + "new": false, + "mode": "FDM", + "deviceName": "Prusa mini", + "imageURL": "", + "internal": 0, + "bedWidth": 180, + "bedDepth": 180, + "bedHeight": 2.5, + "bedRound": false, + "bedBelt": false, + "fwRetract": false, + "filamentSource": "direct", + "originCenter": false, + "deviceZMax": 0, + "maxHeight": 180, + "gcodePre": [ + "G90 ; use absolute coordinates", + "M83 ; extruder relative mode", + "M104 S170 ; set extruder temp for bed leveling", + "M140 S{bed_temp} ; set bed temp", + "M109 R170 ; wait for bed leveling temp", + "M190 S{bed_temp} ; wait for bed temp", + "G28 ; home all without mesh bed level", + "G29 ; mesh bed leveling ", + "M104 S{temp} ; set extruder temp", + "G92 E0.0", + "G1 Y-2.0 X179 F2400", + "G1 Z3 F720", + "M109 S{temp} ; wait for extruder temp", + " ; intro line", + "G1 X170 F1000 ; purge line", + "G1 Z0.2 F720 ; purge line", + "G1 X110.0 E8.0 F900 ; purge line", + "G1 X40.0 E10.0 F700 ; purge line", + "G92 E0.0", + "M221 S95 ; set flow" + ], + "gcodePost": [ + "G1 E-1 F2100 ; retract", + "G1 Z178 Y178 F4200 ; park print head", + "G4 ; wait", + "M104 S0 ; turn off temperature", + "M140 S0 ; turn off heatbed", + "M107 ; turn off fan", + "M221 S100 ; reset flow", + "M900 K0 ; reset LA", + "M84 ; disable motors" + ], + "gcodeProc": "", + "gcodeFan": [], + "gcodeTrack": [], + "gcodeLayer": [ + ";AFTER_LAYER_CHANGE", + ";[layer_z]" + ], + "gcodeFExt": null, + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "extras": { + "palette": { + "printer": "d13ef1ba370f", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + }, + "profiles": [ + { + "processName": "Prusa Mini 0.20mm", + "sliceHeight": 0.2, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceLineWidth": 0.45, + "sliceFillGrow": 0, + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.4, + "sliceFillSparse": 0.2, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSolidRate": 0, + "sliceFillType": "hex", + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 1, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.1, + "sliceSupportSpan": 5, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSupportEnable": true, + "sliceSupportOutline": true, + "sliceSolidMinArea": 1, + "sliceSolidLayers": 3, + "sliceBottomLayers": 4, + "sliceTopLayers": 5, + "firstSliceHeight": 0.2, + "firstLayerRate": 8, + "firstLayerFillRate": 35, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 2, + "firstLayerYOffset": 0, + "firstLayerNozzleTemp": 215, + "firstLayerBedTemp": 60, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFanSpeed": 0, + "firstLayerFlatten": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputDraftShield": false, + "outputTemp": 215, + "outputBedTemp": 60, + "outputFeedrate": 40, + "outputFinishrate": 50, + "outputSeekrate": 150, + "outputShellMult": 1.25, + "outputFillMult": 1.25, + "outputSparseMult": 1.25, + "outputFanSpeed": 255, + "outputRetractDist": 3.2, + "outputRetractSpeed": 70, + "outputRetractWipe": 0, + "outputRetractDwell": 20, + "outputBrimCount": 2, + "outputBrimOffset": 2, + "outputShortPoly": 100, + "outputMinSpeed": 10, + "outputCoastDist": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputAvoidGaps": true, + "outputLayerRetract": false, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "sliceDetectThin": "off", + "sliceMinHeight": 0, + "sliceAdaptive": false, + "zHopDistance": 0.2, + "arcTolerance": 0, + "antiBacklash": 1, + "ranges": [], + "beltAnchor": 3 + } + ] +} diff --git a/src2/kiri-dev/fdm/Raise3D.N2.json b/src2/kiri-dev/fdm/Raise3D.N2.json new file mode 100644 index 00000000..2935b2dd --- /dev/null +++ b/src2/kiri-dev/fdm/Raise3D.N2.json @@ -0,0 +1,65 @@ +{ + "pre":[ + "M140 S{bed_temp}", + "M221 T{tool} S100.00", + "M104 T{tool} S{temp}", + "M109 T{tool} S{temp}", + "M190 S{bed_temp}", + "G21", + "G90", + "M82", + "M107", + "G28 X0 Y0", + "G28 Z0", + "T{tool}", + "G1 Z0.05 F6000.00", + "G92 E0", + "G1 F140 E29", + "G4 P500", + "G1 X15 Y15 Z0.02 F2000", + "G1 F6000.00", + "M117 Printing...", + "M1001" + ], + "post":[ + "G91", + "G1 Z10", + "G90", + "M1002", + "M104 T{tool} S0", + "M104 S0", + "M140 S0", + "G1 X20 Y20 F6000", + "G1 X0 Y0", + "M84", + ";Print Time: {time}", + ";Material#1 Used: {material}", + ";Material#2 Used: 0.0" + ], + "pause":[ + "; --- pause begin ---", + "G91 ; Relative Positioning", + "G0 Z20 ; Move Bed down 20mm", + "G90 ; Absolute positioning", + "G0 X10 Y10 ; Move to 10,10", + "M2000 ; Pause command", + "G91 ; Relative Positioning", + "G0 Z-20 ; Move Bed up 20mm", + "G90 ; Absolute positioning", + "; --- pause end ---" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}", + "layer": [ + ";LAYER:{layer}", + ";HEIGHT:{height}" + ] + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 310, + "bed_depth": 310, + "build_height": 305 + } +} diff --git a/src2/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json b/src2/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json new file mode 100644 index 00000000..2ea7be24 --- /dev/null +++ b/src2/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json @@ -0,0 +1,312 @@ +{ + "mode": "FDM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 110, + "bedDepth": 110, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "gcodeTime": 1, + "maxHeight": 110, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeFeature": [], + "gcodeTrack": [], + "gcodeLayer": [ + "G92 E0" + ], + "gcodePre": [ + "M190 S{bed_temp} ; wait for bed to reach target temp", + "M109 S{temp} ; wait for extruder to reach target temp", + "PRINT_START EXTRUDER={temp} BED={bed_temp}" + ], + "gcodePost": [ + "PRINT_END" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "new": false, + "deviceName": "Rolohaun Rook MK1 LDO", + "imageURL": "", + "fwRetract": false, + "filamentSource": "direct", + "profiles": [ + { + "processName": "0.2 Standard", + "sliceAngle": 45, + "sliceHeight": 0.2, + "sliceShells": 4, + "sliceShellOrder": "alternate", + "sliceLayerStart": "last", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceLineWidth": 0, + "sliceFillGrow": 0, + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillRepeat": 1, + "sliceFillRate": 150, + "sliceSolidRate": 150, + "sliceFillType": "hex", + "sliceSupportDensity": 0.1, + "sliceSupportOffset": 1, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.1, + "sliceSupportSpan": 5, + "sliceSupportGrow": 0, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 4, + "firstSliceHeight": 0.2, + "firstLayerRate": 50, + "firstLayerFillRate": 105, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1.2, + "firstLayerYOffset": 0, + "firstLayerNozzleTemp": 220, + "firstLayerBedTemp": 60, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFanSpeed": 0, + "firstLayerFlatten": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputDraftShield": false, + "outputTemp": 200, + "outputBedTemp": 60, + "outputFeedrate": 150, + "outputFinishrate": 150, + "outputSeekrate": 500, + "outputShellMult": 1, + "outputFillMult": 1, + "outputSparseMult": 1, + "outputFanLayer": 1, + "outputFanSpeed": 255, + "outputRetractDist": 2.9, + "outputRetractSpeed": 50, + "outputRetractWipe": 1, + "outputRetractDwell": 20, + "outputBrimCount": 2, + "outputBrimOffset": 2, + "outputShortPoly": 100, + "outputMinSpeed": 10, + "outputCoastDist": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputAvoidGaps": true, + "outputAlternating": false, + "outputLayerRetract": false, + "outputNozzle": 0, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "sliceDetectThin": "off", + "sliceMinHeight": 0, + "sliceAdaptive": false, + "zHopDistance": 0.2, + "arcTolerance": 0, + "antiBacklash": 0, + "ranges": [] + },{ + "processName": "0.26 Draft", + "sliceAngle": 45, + "sliceHeight": 0.26, + "sliceShells": 4, + "sliceShellOrder": "alternate", + "sliceLayerStart": "last", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceLineWidth": 0, + "sliceFillGrow": 0, + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillRepeat": 1, + "sliceFillRate": 150, + "sliceSolidRate": 150, + "sliceFillType": "hex", + "sliceSupportDensity": 0.1, + "sliceSupportOffset": 1, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.1, + "sliceSupportSpan": 5, + "sliceSupportGrow": 0, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstSliceHeight": 0.2, + "firstLayerRate": 50, + "firstLayerFillRate": 105, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1.2, + "firstLayerYOffset": 0, + "firstLayerNozzleTemp": 220, + "firstLayerBedTemp": 60, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFanSpeed": 0, + "firstLayerFlatten": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputDraftShield": false, + "outputTemp": 200, + "outputBedTemp": 60, + "outputFeedrate": 150, + "outputFinishrate": 100, + "outputSeekrate": 500, + "outputShellMult": 1, + "outputFillMult": 1, + "outputSparseMult": 1, + "outputFanLayer": 1, + "outputFanSpeed": 255, + "outputRetractDist": 2.9, + "outputRetractSpeed": 50, + "outputRetractWipe": 1, + "outputRetractDwell": 20, + "outputBrimCount": 2, + "outputBrimOffset": 2, + "outputShortPoly": 100, + "outputMinSpeed": 10, + "outputCoastDist": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputAvoidGaps": true, + "outputAlternating": false, + "outputLayerRetract": false, + "outputNozzle": 0, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "sliceDetectThin": "off", + "sliceMinHeight": 0, + "sliceAdaptive": false, + "zHopDistance": 0.2, + "arcTolerance": 0, + "antiBacklash": 0, + "ranges": [] + },{ + "processName": "0.1 Fine", + "sliceAngle": 45, + "sliceHeight": 0.1, + "sliceShells": 4, + "sliceShellOrder": "alternate", + "sliceLayerStart": "last", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceLineWidth": 0, + "sliceFillGrow": 0, + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.25, + "sliceFillSparse": 0.1, + "sliceFillRepeat": 1, + "sliceFillRate": 150, + "sliceSolidRate": 150, + "sliceFillType": "hex", + "sliceSupportDensity": 0.1, + "sliceSupportOffset": 1, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.1, + "sliceSupportSpan": 5, + "sliceSupportGrow": 0, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 4, + "sliceTopLayers": 5, + "firstSliceHeight": 0.2, + "firstLayerRate": 50, + "firstLayerFillRate": 105, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1.2, + "firstLayerYOffset": 0, + "firstLayerNozzleTemp": 220, + "firstLayerBedTemp": 60, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFanSpeed": 0, + "firstLayerFlatten": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputDraftShield": false, + "outputTemp": 200, + "outputBedTemp": 60, + "outputFeedrate": 80, + "outputFinishrate": 80, + "outputSeekrate": 500, + "outputShellMult": 1, + "outputFillMult": 1, + "outputSparseMult": 1, + "outputFanLayer": 1, + "outputFanSpeed": 255, + "outputRetractDist": 2.9, + "outputRetractSpeed": 50, + "outputRetractWipe": 1, + "outputRetractDwell": 20, + "outputBrimCount": 2, + "outputBrimOffset": 2, + "outputShortPoly": 100, + "outputMinSpeed": 10, + "outputCoastDist": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputAvoidGaps": true, + "outputAlternating": false, + "outputLayerRetract": false, + "outputNozzle": 0, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "sliceDetectThin": "off", + "sliceMinHeight": 0, + "sliceAdaptive": false, + "zHopDistance": 0.2, + "arcTolerance": 0, + "antiBacklash": 0, + "ranges": [] + } + ], + "extras": {} +} \ No newline at end of file diff --git a/src2/kiri-dev/fdm/SainSmart.Infi-20.json b/src2/kiri-dev/fdm/SainSmart.Infi-20.json new file mode 100644 index 00000000..b6635c1f --- /dev/null +++ b/src2/kiri-dev/fdm/SainSmart.Infi-20.json @@ -0,0 +1,165 @@ +{ + "pre":[ + "G28 XY ; home XY axes", + "G1 Y50 F1000 ; lift y axis off belt for heating", + "M107 ; turn off filament cooling fan", + "M140 S{bed_temp} ; set bed temp", + "M104 S{temp} ; set hot end temp", + "M105 ; report temps", + "M190 S{bed_temp} ; wait for bed", + "M105 ; report temps", + "M109 S{temp} ; wait for hot end", + "M82 ; absolute extrusion mode", + "G90 ; absolute positioning mode", + "G21 ; use metric values", + "G92 E0 ; reset filament position", + "G1 Y0.15 ; move head to belt", + "G1 X190 E50 F800 ; extruder purge line", + "G1 Z0.3 ; shift belt away 0.3mm", + "G1 X0 E100 ; extruder purge line", + "G92 Z0 E0 ; reset bed + extruder position", + "M117 CR-30 Printing..." + ], + "post":[ + "M107 ; disable cooling fan", + "M106 P0 S0 ; disable cooling fan", + "M104 S0 ; shut down hot end", + "M140 S0 ; shut down bed", + "G92 E1 ; set filament position", + "G1 E-1 F300 ; retract filament", + "G28 X ; home X axis", + "G1 Y50 F1000 ; lift y axis", + "M84 ; disable stepper motors" + ], + "extruders":[ + { + "nozzle": 0.4, + "filament": 1.75, + "offset_x": 0, + "offset_y": 0 + } + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}", + "progress": "M117 {progress}% layer {layer} / {layers}", + "layer": [ + ";; --- layer {layer} {height} @ {z} ---", + "M117 {progress}% layer {layer} / {layers}" + ] + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_belt": true, + "bed_width": 200, + "bed_depth": 300, + "build_height": 170 + }, + "profiles":[{ + "processName": "SSI20 PLA", + "sliceHeight": 0.25, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.3, + "sliceFillSparse": 0.2, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 15, + "sliceSolidLayers": 2, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1.0, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 0, + "firstLayerFanSpeed": 0, + "outputTemp": 210, + "outputFanMax": 255, + "outputBedTemp": 70, + "outputFanSpeed": 255, + "outputFeedrate": 30, + "outputFinishrate": 30, + "outputSeekrate": 80, + "outputShellMult": 1.15, + "outputFillMult": 1.15, + "outputSparseMult": 1.15, + "outputFanLayer": 0, + "outputRetractDist": 6, + "outputRetractSpeed": 70, + "outputRetractWipe": 0, + "outputRetractDwell": 0, + "outputShortPoly": 50.0, + "outputMinSpeed": 5.0, + "outputCoastDist": 0.1, + "outputWipeDistance": 0, + "outputLayerRetract": false, + "detectThinWalls": false, + "zHopDistance": 0, + "antiBacklash": 0 + },{ + "processName": "SSI20 PETG", + "sliceHeight": 0.25, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.3, + "sliceFillSparse": 0.2, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 0, + "sliceSupportSize": 6, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 1, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 15, + "sliceSolidLayers": 2, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstLayerRate": 10, + "firstLayerPrintMult": 1.0, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 0, + "firstLayerFanSpeed": 0, + "outputTemp": 240, + "outputFanMax": 0, + "outputBedTemp": 70, + "outputFanSpeed": 0, + "outputFeedrate": 30, + "outputFinishrate": 30, + "outputSeekrate": 80, + "outputShellMult": 1.15, + "outputFillMult": 1.15, + "outputSparseMult": 1.15, + "outputFanLayer": 0, + "outputRetractDist": 6, + "outputRetractSpeed": 70, + "outputRetractWipe": 0, + "outputRetractDwell": 0, + "outputShortPoly": 50.0, + "outputMinSpeed": 5.0, + "outputCoastDist": 0.1, + "outputWipeDistance": 0, + "outputLayerRetract": false, + "detectThinWalls": false, + "zHopDistance": 0, + "antiBacklash": 0 + }] +} diff --git a/src2/kiri-dev/fdm/Snapmaker.Original.json b/src2/kiri-dev/fdm/Snapmaker.Original.json new file mode 100644 index 00000000..adb9a559 --- /dev/null +++ b/src2/kiri-dev/fdm/Snapmaker.Original.json @@ -0,0 +1,38 @@ +{ + "pre":[ + "M117 Heating ; announce heating", + "M107 ; turn off filament cooling fan", + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "G28 ; home axes", + "G1 F3000 ; set default move speed", + "M190 S{bed_temp} T0 ; wait for bed to reach target temp", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "M117 Purge ; announce purge", + "G92 E0 ; set extruder position to 0", + "M117 Start Print ; announce start of print" + ], + "post":[ + "M117 Done ; announce done", + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off extruder", + "M140 S0 T0 ; turn off bed", + "G91 ; relative movement", + "G1 Z2 F1200 ; drop bed 2mm", + "G90 ; absolute positioning", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}", + "progress": "M117 {progress}% layer {layer} / {layers}" + }, + "settings":{ + "origin_center": false, + "bed_width": 125, + "bed_depth": 125, + "build_height": 125 + } +} diff --git a/src2/kiri-dev/fdm/Tina2.Basic.json b/src2/kiri-dev/fdm/Tina2.Basic.json new file mode 100644 index 00000000..7be323b8 --- /dev/null +++ b/src2/kiri-dev/fdm/Tina2.Basic.json @@ -0,0 +1,176 @@ +{ + "mode": "FDM", + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedWidth": 100, + "bedDepth": 120, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "gcodeTime": 1, + "maxHeight": 100, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeFeature": [], + "gcodeTrack": [ + "M73 {progress}" + ], + "gcodeLayer": [ + "M117 Layer {layer}/{layers}" + ], + "gcodePre": [ + ";(**** start.gcode for tina2****)", + "M203 Z15", + "M104 S{temp}", + "G28 Z", + "G28 X Y; Home extruder", + "G1 X55 Y55 F1000", + "G29", + "M107 ; Turn off fan", + "G90 ; Absolute positioning", + "M82 ; Extruder in absolute mode", + "M109 S{temp} ; wait for extruder to reach target temp", + "G92 E0 ; Reset extruder position", + "G1 X90 Y6 Z0.27 F2000", + "G1 X20 Y6 Z0.27 E15 F1000", + "G92 E0 ; Reset extruder position", + "M203 Z5" + ], + "gcodePost": [ + ";(**** end.gcode for tina2****)", + "M203 Z15", + "M104 S0", + "M107", + "G92 E0 (Reset after prime)", + "G0 E-1 F300", + "G28 Z F300", + "G28 X0 Y0", + "G1 Y90 F1000" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + }, + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "new": false, + "deviceName": "Tina2 Basic", + "fwRetract": false, + "filamentSource": "direct", + "profiles": [ + { + "processName": "Tina2 PLA Profile", + "sliceAngle": 45, + "sliceHeight": 0.2, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceLineWidth": 0, + "sliceFillGrow": 0, + "sliceFillAngle": 45, + "sliceFillWidth": 1, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillRepeat": 1, + "sliceFillRate": 0, + "sliceSolidRate": 0, + "sliceFillType": "grid", + "sliceSupportDensity": 0.1, + "sliceSupportOffset": 1, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.1, + "sliceSupportSpan": 5, + "sliceSupportGrow": 0, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSupportEnable": false, + "sliceSupportOutline": true, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 3, + "firstSliceHeight": 0.2, + "firstLayerRate": 30, + "firstLayerFillRate": 35, + "firstLayerPrintMult": 1, + "firstLayerLineMult": 1, + "firstLayerYOffset": 0, + "firstLayerNozzleTemp": 200, + "firstLayerBedTemp": 0, + "firstLayerBrim": 0, + "firstLayerBrimIn": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "firstLayerBeltLead": 3, + "firstLayerBeltBump": 0, + "firstLayerFanSpeed": 0, + "firstLayerFlatten": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputDraftShield": false, + "outputTemp": 200, + "outputBedTemp": 0, + "outputFeedrate": 50, + "outputFinishrate": 50, + "outputSeekrate": 80, + "outputShellMult": 1.25, + "outputFillMult": 1.25, + "outputSparseMult": 1.25, + "outputFanLayer": 1, + "outputFanSpeed": 255, + "outputRetractDist": 1.5, + "outputRetractSpeed": 40, + "outputRetractWipe": 0, + "outputRetractDwell": 20, + "outputBrimCount": 2, + "outputBrimOffset": 2, + "outputShortPoly": 100, + "outputMinSpeed": 10, + "outputCoastDist": 0, + "outputPurgeTower": 0, + "outputBeltFirst": false, + "outputAvoidGaps": true, + "outputAlternating": false, + "outputLayerRetract": false, + "outputNozzle": 0, + "outputLoops": 0, + "outputInvertX": false, + "outputInvertY": false, + "sliceDetectThin": "off", + "sliceMinHeight": 0, + "sliceAdaptive": false, + "zHopDistance": 0.2, + "arcTolerance": 0, + "antiBacklash": 0, + "ranges": [] + } + ], + "extras": { + "palette": { + "printer": "54ac97df7bd4", + "ping": null, + "feed": null, + "push": null, + "offset": null, + "heat": null, + "cool": null, + "press": null + } + } +} diff --git a/src2/kiri-dev/fdm/TypeA.Series1.json b/src2/kiri-dev/fdm/TypeA.Series1.json new file mode 100644 index 00000000..846b1c40 --- /dev/null +++ b/src2/kiri-dev/fdm/TypeA.Series1.json @@ -0,0 +1,37 @@ +{ + "pre":[ + "G1 Z15.0 F12000 ; move the platform down 15mm", + "G1 X150 Y5 F12000 ; center extruder on X", + "M107 ; turn off filament cooling fan", + "M83 ; use relative positioning for extruder", + "M104 S{temp} ; set extruder temperature", + "G21 ; use metric values", + "G90 ; use absolute positioning mode", + "G28 ; move to endstops", + "G29 ; perform auto-levelling", + "M109 S{temp} ; wait for extruder to reach target temp", + "G1 X150 Y5 Z0.3 ; move the platform to purge extrusion", + "G92 E0 ; zero the extruded length", + "G1 F200 X250 E30 ; extrude 30mm of feed stock", + "G92 E0 ; zero the extruded length again", + "G1 X150 Y150 Z25 F12000 ; center head for print", + "G1 F12000 ; set head seek rate" + ], + "post":[ + "G1 E-1 F300 ; retract the filament to release pressure", + "G1 Z{z_max} E-5 F1200 ; drop bed, retract more filament", + "G28 X0 Y0 ; move X/Y to min endstops", + "M107 ; turn off filament cooling fan", + "M104 S0 ; turn off extruder", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 305, + "bed_depth": 305, + "build_height": 305 + } +} diff --git a/src2/kiri-dev/fdm/TypeA.Series1Pro.json b/src2/kiri-dev/fdm/TypeA.Series1Pro.json new file mode 100644 index 00000000..c282a151 --- /dev/null +++ b/src2/kiri-dev/fdm/TypeA.Series1Pro.json @@ -0,0 +1,40 @@ +{ + "pre":[ + "G1 Z15.0 F12000 ; move the platform down 15mm", + "G1 X150 Y5 F12000 ; center extruder on X", + "M107 ; turn off filament cooling fan", + "M83 ; use relative positioning for extruder", + "M104 S{temp} ; set extruder temperature", + "M140 S{bed_temp} ; set bed temperature", + "G21 ; use metric values", + "G90 ; use absolute positioning mode", + "G28 ; move to endstops", + "G29 ; perform auto-levelling", + "M109 S{temp} ; wait for extruder to reach target temp", + "M190 S{bed_temp} ; wait for bed to reach target temp", + "G1 X150 Y5 Z0.3 ; move the platform to purge extrusion", + "G92 E0 ; zero the extruded length", + "G1 F200 X250 E30 ; extrude 30mm of feed stock", + "G92 E0 ; zero the extruded length again", + "G1 X150 Y150 Z25 F12000 ; center head for print", + "G1 F12000 ; set head seek rate" + ], + "post":[ + "G1 E-1 F300 ; retract the filament to release pressure", + "G1 Z{z_max} E-5 F1200 ; drop bed, retract more filament", + "G28 X0 Y0 ; move X/Y to min endstops", + "M107 ; turn off filament cooling fan", + "M104 S0 ; turn off extruder", + "M140 S0 ; turn off bed", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 305, + "bed_depth": 305, + "build_height": 305 + } +} diff --git a/src2/kiri-dev/fdm/ULIO.3D.BRIX.json b/src2/kiri-dev/fdm/ULIO.3D.BRIX.json new file mode 100644 index 00000000..113f20a7 --- /dev/null +++ b/src2/kiri-dev/fdm/ULIO.3D.BRIX.json @@ -0,0 +1,144 @@ +{ + "mode": "FDM", + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedWidth": 100, + "bedDepth": 50, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "gcodeTime": 1, + "maxHeight": 50.1, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeFeature": [ + "M73 P{progress}" + ], + "gcodeTrack": [], + "gcodeLayer": [], + "gcodePre": [ + "G90 ; use absolute coordinates", + "M83 ; extruder relative mode", + "M104 S{temp} ; set extruder temp", + "M109 S{temp} ; wait for extruder temp", + "G28 W ; home all without mesh bed level", + "G92 E0.0", + "G1 F{600} ", + "M425 X0 Y0 Z0 ; Set backlash to specific values for all axis", + "M425 F1 S3 ; Enable backlash compensation at 100% for 3 mm", + "M117 ULIO 3D BRIX Printing" + ], + "gcodePost": [ + "M104 S0 ; turn off right extruder", + "M107 ; turn off filament cooling fan", + "G1 Z50 F{180} ; drop bed G1 50 mm ", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "new": false, + "deviceName": "ULIO 3D BRIX", + "fwRetract": false, + "filamentSource": "direct", + "profiles": [ + { + "processName": "ULIO 3D BRIX PLA", + "sliceHeight": 0.25, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "center", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.25, + "sliceFillType": "grid", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.5, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 50, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 5, + "firstLayerRate": 20, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 200, + "firstLayerFanSpeed": 0, + "outputTemp": 215, + "outputBedTemp": 0, + "outputFanSpeed": 255, + "outputFeedrate": 35, + "outputFinishrate": 20, + "outputSeekrate": 35, + "outputShellMult": 1.05, + "outputFillMult": 1.1, + "outputSparseMult": 1.1, + "outputRetractDist": 3.2, + "outputRetractSpeed": 80, + "outputRetractWipe": 2, + "outputRetractDwell": 0, + "outputShortPoly": 50, + "outputMinSpeed": 1, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.25, + "firstLayerFillRate": 20, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": true, + "outputRaftSpacing": 0.2, + "outputBrimCount": 5, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "outputLoops": 0, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "sliceFillRepeat": 4, + "sliceLineWidth": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": false, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceSupportGrow": 0, + "outputAlternating": false, + "outputFanLayer": 5 + } + ] +} diff --git a/src2/kiri-dev/fdm/ULIO.3D.STEAM.json b/src2/kiri-dev/fdm/ULIO.3D.STEAM.json new file mode 100644 index 00000000..b6239564 --- /dev/null +++ b/src2/kiri-dev/fdm/ULIO.3D.STEAM.json @@ -0,0 +1,146 @@ +{ + "mode": "FDM", + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedWidth": 100, + "bedDepth": 100, + "bedRound": false, + "bedBelt": false, + "deviceZMax": 0, + "gcodeTime": 1, + "maxHeight": 55, + "originCenter": false, + "gcodeFan": [ + "M106 S{fan_speed}" + ], + "gcodeFeature": [ + "M73 P{progress}" + ], + "gcodeTrack": [], + "gcodeLayer": [], + "gcodePre": [ + "G90 ; use absolute coordinates", + "M83 ; extruder relative mode", + "M104 S{temp} ; set extruder temp", + "M109 S{temp} ; wait for extruder temp", + "G28 W ; home all without mesh bed level", + "G92 E0.0", + "G1 F{600} ", + "M425 X0 Y0 Z0 ; Set backlash to specific values for all axis", + "M425 F1 S3 ; Enable backlash compensation at 100% for 3 mm", + "M117 ULIO 3D STEAM Printing" + ], + "gcodePost": [ + "M104 S0 ; turn off right extruder", + "M107 ; turn off filament cooling fan", + "G1 Z55 F{180} ; drop bed G1 55 mm ", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.4, + "extOffsetX": 0, + "extOffsetY": 0 + } + ], + "new": false, + "deviceName": "ULIO 3D STEAM", + "fwRetract": false, + "filamentSource": "direct", + "profiles": [ + { + "processName": "ULIO 3D STEAM PLA", + "sliceHeight": 0.25, + "sliceShells": 3, + "sliceShellOrder": "in-out", + "sliceLayerStart": "center", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.25, + "sliceFillType": "grid", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.25, + "sliceSupportOffset": 0.5, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 45, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 1, + "sliceBottomLayers": 3, + "sliceTopLayers": 5, + "firstLayerRate": 20, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 200, + "firstLayerFanSpeed": 0, + "outputTemp": 215, + "outputBedTemp": 0, + "outputFanSpeed": 255, + "outputFeedrate": 35, + "outputFinishrate": 20, + "outputSeekrate": 35, + "outputShellMult": 1.05, + "outputFillMult": 1.1, + "outputSparseMult": 1.1, + "outputRetractDist": 3.2, + "outputRetractSpeed": 80, + "outputRetractWipe": 2, + "outputRetractDwell": 0, + "outputShortPoly": 50, + "outputMinSpeed": 1, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.25, + "firstLayerFillRate": 20, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 0, + "firstLayerBedTemp": 0, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": true, + "outputRaftSpacing": 0.2, + "outputBrimCount": 5, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "outputLoops": 0, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "sliceFillRepeat": 4, + "sliceLineWidth": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": false, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceSupportGrow": 0, + "outputAlternating": false, + "outputFanLayer": 5, + "sliceAngle": 45, + "outputNozzle": 0 + } + ] +} diff --git a/src2/kiri-dev/fdm/Ultimaker.Ultimaker2.json b/src2/kiri-dev/fdm/Ultimaker.Ultimaker2.json new file mode 100644 index 00000000..e353b81e --- /dev/null +++ b/src2/kiri-dev/fdm/Ultimaker.Ultimaker2.json @@ -0,0 +1,40 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G90 ; set absolute positioning mode", + "M82 ; set absolute positioning for extruder", + "M107 ; turn off filament cooling fan", + "G92 E0 ; zero the extruded", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G1 E15 F200 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G1 Z205 F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "extruders":[ + { + "nozzle": 0.4, + "filament": 2.85, + "offset_x": 0, + "offset_y": 0 + } + ], + "settings":{ + "origin_center": false, + "bed_width": 223, + "bed_depth": 223, + "build_height": 205, + "extrude_abs": true + } +} diff --git a/src2/kiri-dev/fdm/Wanhao.Duplicator_4s.json b/src2/kiri-dev/fdm/Wanhao.Duplicator_4s.json new file mode 100644 index 00000000..11ce3cb9 --- /dev/null +++ b/src2/kiri-dev/fdm/Wanhao.Duplicator_4s.json @@ -0,0 +1,33 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 225, + "bed_depth": 145, + "build_height": 150 + } +} diff --git a/src2/kiri-dev/fdm/Wanhao.Duplicator_5s.json b/src2/kiri-dev/fdm/Wanhao.Duplicator_5s.json new file mode 100644 index 00000000..d082dc96 --- /dev/null +++ b/src2/kiri-dev/fdm/Wanhao.Duplicator_5s.json @@ -0,0 +1,33 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 305, + "bed_depth": 205, + "build_height": 575 + } +} diff --git a/src2/kiri-dev/fdm/Wanhao.Duplicator_6.json b/src2/kiri-dev/fdm/Wanhao.Duplicator_6.json new file mode 100644 index 00000000..a7d69a14 --- /dev/null +++ b/src2/kiri-dev/fdm/Wanhao.Duplicator_6.json @@ -0,0 +1,33 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 200, + "bed_depth": 200, + "build_height": 180 + } +} diff --git a/src2/kiri-dev/fdm/Wanhao.Duplicator_i3.json b/src2/kiri-dev/fdm/Wanhao.Duplicator_i3.json new file mode 100644 index 00000000..a7d69a14 --- /dev/null +++ b/src2/kiri-dev/fdm/Wanhao.Duplicator_i3.json @@ -0,0 +1,33 @@ +{ + "pre":[ + "M104 S{temp} T{tool} ; set extruder temperature", + "M140 S{bed_temp} T{tool} ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T{tool} ; wait for extruder to reach target temp", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T{tool} ; turn off right extruder", + "M140 S0 T{tool} ; turn off bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 200, + "bed_depth": 200, + "build_height": 180 + } +} diff --git a/src2/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json b/src2/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json new file mode 100644 index 00000000..f9cd43bd --- /dev/null +++ b/src2/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json @@ -0,0 +1,31 @@ +{ + "pre":[ + "T{tool} ; select extruder #0", + "M104 S{temp} ; set nozzle temp", + "M109 S{temp} ; wait for nozzle to heat", + "M82 ; absolute extrusion mode", + "G28 ; home", + "G1 Z15 F6000 ; lower platform 15mm", + "G92 E0 ; zero out extruder position", + "G1 F200 E3 ; prime the extruder 3mm" + ], + "post":[ + "M107 ; fan off", + "M104 S0 ; set nozzle temp to 0", + "M140 S0 ; set bed temp to 0", + "G92 E0 ; zero out extruder position", + "G1 E-1 F300 ; retract filament", + "G28 X0 Y0 ; home X Y", + "M84 ; disable steppers" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 150, + "bed_depth": 150, + "build_height": 150 + } +} diff --git a/src2/kiri-dev/fdm/XYZ.daVinci.Mini.w.json b/src2/kiri-dev/fdm/XYZ.daVinci.Mini.w.json new file mode 100644 index 00000000..be80197d --- /dev/null +++ b/src2/kiri-dev/fdm/XYZ.daVinci.Mini.w.json @@ -0,0 +1,32 @@ +{ + "pre":[ + "T{tool} ; select extruder #0", + "M104 S{temp} ; set nozzle temp", + "M109 S{temp} ; wait for nozzle to heat", + "G21 ; set units to millimeters", + "M82 ; absolute extrusion mode", + "G28 ; home", + "G1 Z15 F6000 ; lower platform 15mm", + "G92 E0 ; zero out extruder position", + "G1 F200 E3 ; prime the extruder 3mm" + ], + "post":[ + "M107 ; fan off", + "M104 S0 ; set nozzle temp to 0", + "M140 S0 ; set bed temp to 0", + "G92 E0 ; zero out extruder position", + "G1 E-1 F300 ; retract filament", + "G28 X0 Y0 ; home X Y", + "M84 ; disable steppers" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "extrude_abs": true, + "bed_width": 150, + "bed_depth": 150, + "build_height": 150 + } +} diff --git a/src2/kiri-dev/laser/Any.Generic.Laser.json b/src2/kiri-dev/laser/Any.Generic.Laser.json new file mode 100644 index 00000000..5ede1faa --- /dev/null +++ b/src2/kiri-dev/laser/Any.Generic.Laser.json @@ -0,0 +1,21 @@ +{ + "file-ext": "gcode", + "token-space": " ", + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "laser-on":[ + "M106 S{power}" + ], + "laser-off":[ + "M107" + ], + "settings": { + "bed_width": 300, + "bed_depth": 300 + } +} diff --git a/src2/kiri-dev/laser/Glowforge.json b/src2/kiri-dev/laser/Glowforge.json new file mode 100644 index 00000000..89e2c030 --- /dev/null +++ b/src2/kiri-dev/laser/Glowforge.json @@ -0,0 +1,12 @@ +{ + "file-ext": "gcode", + "token-space": " ", + "pre":[], + "post":[], + "laser-on":[], + "laser-off":[], + "settings": { + "bed_width": 500, + "bed_depth": 300 + } +} diff --git a/src2/kiri-dev/laser/Snapmaker.A250T.json b/src2/kiri-dev/laser/Snapmaker.A250T.json new file mode 100644 index 00000000..e300378d --- /dev/null +++ b/src2/kiri-dev/laser/Snapmaker.A250T.json @@ -0,0 +1,35 @@ +{ + "file-ext": "gcode", + "token-space": " ", + "pre":[ + "G90 ; absolute positioning", + "G21 ; metric units", + "M106 P0 S255 ; fan on", + "M3 P{power} S255 ; set default laser power", + "M5 ; laser off", + "G0 F3000 ; default feed rates", + "G1 F800 ; default feed rates" + ], + "post":[ + "M5 ; laser off", + "M107 P0 ; fan off", + "G91 ; relative positioning", + "G0 Z0 F150 ; raise laser", + "G90 ; absolute positioning" + ], + "laser-on":[ + "M3 P{power}" + ], + "laser-off":[ + "M5" + ], + "settings": { + "origin_center": false, + "bed_width": 230, + "bed_depth": 235 + }, + "profiles":[{ + "processName": "Snapmaker Laser", + "ctOriginCenter": false + }] +} \ No newline at end of file diff --git a/src2/kiri-dev/laser/Snapmaker.A350T.json b/src2/kiri-dev/laser/Snapmaker.A350T.json new file mode 100644 index 00000000..0817ac2b --- /dev/null +++ b/src2/kiri-dev/laser/Snapmaker.A350T.json @@ -0,0 +1,35 @@ +{ + "file-ext": "gcode", + "token-space": " ", + "pre":[ + "G90 ; absolute positioning", + "G21 ; metric units", + "M106 P0 S255 ; fan on", + "M3 P{power} S255 ; set default laser power", + "M5 ; laser off", + "G0 F3000 ; default feed rates", + "G1 F800 ; default feed rates" + ], + "post":[ + "M5 ; laser off", + "M107 P0 ; fan off", + "G91 ; relative positioning", + "G0 Z0 F150 ; raise laser", + "G90 ; absolute positioning" + ], + "laser-on":[ + "M3 P{power}" + ], + "laser-off":[ + "M5" + ], + "settings": { + "origin_center": false, + "bed_width": 320, + "bed_depth": 335 + }, + "profiles":[{ + "processName": "Snapmaker Laser", + "ctOriginCenter": false + }] +} diff --git a/src2/kiri-dev/laser/Snapmaker.Original.json b/src2/kiri-dev/laser/Snapmaker.Original.json new file mode 100644 index 00000000..22b4aae9 --- /dev/null +++ b/src2/kiri-dev/laser/Snapmaker.Original.json @@ -0,0 +1,35 @@ +{ + "file-ext": "gcode", + "token-space": " ", + "pre":[ + "G90 ; absolute positioning", + "G21 ; metric units", + "M106 P0 S255 ; fan on", + "M3 P100 S255 ; set default laser power", + "M5 ; laser off", + "G0 F3000 ; default feed rates", + "G1 F800 ; default feed rates" + ], + "post":[ + "M5 ; laser off", + "M107 P0 ; fan off", + "G91 ; relative positioning", + "G0 Z0 F150 ; raise laser", + "G90 ; absolute positioning" + ], + "laser-on":[ + "M3" + ], + "laser-off":[ + "M5" + ], + "settings": { + "origin_center": false, + "bed_width": 125, + "bed_depth": 125 + }, + "profiles":[{ + "processName": "Snapmaker Laser", + "ctOriginCenter": false + }] +} diff --git a/src2/kiri-dev/laser/Xtool.P2.json b/src2/kiri-dev/laser/Xtool.P2.json new file mode 100644 index 00000000..8fcbda7f --- /dev/null +++ b/src2/kiri-dev/laser/Xtool.P2.json @@ -0,0 +1,12 @@ +{ + "file-ext": "svg", + "token-space": " ", + "pre":[], + "post":[], + "laser-on":[], + "laser-off":[], + "settings": { + "bed_width": 600, + "bed_depth": 308 + } +} \ No newline at end of file diff --git a/src2/kiri-dev/laser/xTool.D1.json b/src2/kiri-dev/laser/xTool.D1.json new file mode 100644 index 00000000..882586ca --- /dev/null +++ b/src2/kiri-dev/laser/xTool.D1.json @@ -0,0 +1,12 @@ +{ + "file-ext": "svg", + "token-space": " ", + "pre":[], + "post":[], + "laser-on":[], + "laser-off":[], + "settings": { + "bed_width": 432, + "bed_depth": 406 + } +} \ No newline at end of file diff --git a/src2/kiri-dev/laser/xTool.F1.Ultra.json b/src2/kiri-dev/laser/xTool.F1.Ultra.json new file mode 100644 index 00000000..df6ca753 --- /dev/null +++ b/src2/kiri-dev/laser/xTool.F1.Ultra.json @@ -0,0 +1,12 @@ +{ + "file-ext": "svg", + "token-space": " ", + "pre":[], + "post":[], + "laser-on":[], + "laser-off":[], + "settings": { + "bed_width": 220, + "bed_depth": 220 + } +} \ No newline at end of file diff --git a/src2/kiri-dev/laser/xTool.F1.json b/src2/kiri-dev/laser/xTool.F1.json new file mode 100644 index 00000000..ef567afc --- /dev/null +++ b/src2/kiri-dev/laser/xTool.F1.json @@ -0,0 +1,12 @@ +{ + "file-ext": "svg", + "token-space": " ", + "pre":[], + "post":[], + "laser-on":[], + "laser-off":[], + "settings": { + "bed_width": 115, + "bed_depth": 115 + } +} \ No newline at end of file diff --git a/src2/kiri-dev/laser/xTool.M1.json b/src2/kiri-dev/laser/xTool.M1.json new file mode 100644 index 00000000..b6bb67a4 --- /dev/null +++ b/src2/kiri-dev/laser/xTool.M1.json @@ -0,0 +1,12 @@ +{ + "file-ext": "svg", + "token-space": " ", + "pre":[], + "post":[], + "laser-on":[], + "laser-off":[], + "settings": { + "bed_width": 385, + "bed_depth": 300 + } +} \ No newline at end of file diff --git a/src2/kiri-dev/laser/xTool.P2S.json b/src2/kiri-dev/laser/xTool.P2S.json new file mode 100644 index 00000000..9b99570b --- /dev/null +++ b/src2/kiri-dev/laser/xTool.P2S.json @@ -0,0 +1,12 @@ +{ + "file-ext": "svg", + "token-space": " ", + "pre":[], + "post":[], + "laser-on":[], + "laser-off":[], + "settings": { + "bed_width": 680, + "bed_depth": 360 + } +} \ No newline at end of file diff --git a/src2/kiri-dev/laser/xTool.S1.json b/src2/kiri-dev/laser/xTool.S1.json new file mode 100644 index 00000000..491908dd --- /dev/null +++ b/src2/kiri-dev/laser/xTool.S1.json @@ -0,0 +1,12 @@ +{ + "file-ext": "svg", + "token-space": " ", + "pre":[], + "post":[], + "laser-on":[], + "laser-off":[], + "settings": { + "bed_width": 608, + "bed_depth": 385 + } +} \ No newline at end of file diff --git a/src2/kiri-dev/sla/Anycubic.Photon.S.json b/src2/kiri-dev/sla/Anycubic.Photon.S.json new file mode 100644 index 00000000..a9b00c57 --- /dev/null +++ b/src2/kiri-dev/sla/Anycubic.Photon.S.json @@ -0,0 +1,10 @@ +{ + "no_clone": true, + "cmd":{}, + "settings":{ + "bed_width": 120.96, + "bed_depth": 68.04, + "bed_height": 1, + "max_height": 150 + } +} diff --git a/src2/kiri-dev/sla/Anycubic.Photon.json b/src2/kiri-dev/sla/Anycubic.Photon.json new file mode 100644 index 00000000..315df91e --- /dev/null +++ b/src2/kiri-dev/sla/Anycubic.Photon.json @@ -0,0 +1,12 @@ +{ + "no_clone": true, + "cmd":{}, + "settings":{ + "bed_width": 120.96, + "bed_depth": 68.04, + "bed_height": 1, + "max_height": 150, + "resolution_x": 2560, + "resolution_y": 1440 + } +} diff --git a/src2/kiri-dev/sla/Creality.Halot.Max.json b/src2/kiri-dev/sla/Creality.Halot.Max.json new file mode 100644 index 00000000..538efb5e --- /dev/null +++ b/src2/kiri-dev/sla/Creality.Halot.Max.json @@ -0,0 +1,12 @@ +{ + "no_clone": true, + "cmd":{}, + "settings":{ + "bed_width": 293, + "bed_depth": 165, + "bed_height": 1, + "max_height": 300, + "resolution_x": 3840, + "resolution_y": 2160 + } +} diff --git a/src2/kiri-dev/sla/Creality.Halot.One.json b/src2/kiri-dev/sla/Creality.Halot.One.json new file mode 100644 index 00000000..d76dd651 --- /dev/null +++ b/src2/kiri-dev/sla/Creality.Halot.One.json @@ -0,0 +1,12 @@ +{ + "no_clone": true, + "cmd":{}, + "settings":{ + "bed_width": 80, + "bed_depth": 127, + "bed_height": 1, + "max_height": 160, + "resolution_x": 1620, + "resolution_y": 2560 + } +} diff --git a/src2/kiri-dev/sla/Creality.Halot.Sky.json b/src2/kiri-dev/sla/Creality.Halot.Sky.json new file mode 100644 index 00000000..d1cb0dc4 --- /dev/null +++ b/src2/kiri-dev/sla/Creality.Halot.Sky.json @@ -0,0 +1,12 @@ +{ + "no_clone": true, + "cmd":{}, + "settings":{ + "bed_width": 192, + "bed_depth": 120, + "bed_height": 1, + "max_height": 200, + "resolution_x": 3840, + "resolution_y": 2400 + } +} diff --git a/src2/kiri-dev/wedm/Any.Generic.WireEDM.json b/src2/kiri-dev/wedm/Any.Generic.WireEDM.json new file mode 100644 index 00000000..4c0266e7 --- /dev/null +++ b/src2/kiri-dev/wedm/Any.Generic.WireEDM.json @@ -0,0 +1,53 @@ +{ + "mode": "WEDM", + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedWidth": 250, + "bedDepth": 150, + "maxHeight": 50, + "gcodePre": [ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "gcodePost": [ + "M30 ; program end" + ], + "gcodeFExt": "gcode", + "gcodeSpace": true, + "gcodeLaserOn": [ + "M106 S{power}" + ], + "gcodeLaserOff": [ + "M107" + ], + "new": false, + "deviceName": "Generic Wire EDM", + "bedBelt": false, + "bedRound": false, + "originCenter": false, + "fwRetract": false, + "profiles": [ + { + "processName": "wire-edm", + "ctSliceKerf": 0.1, + "ctSliceHeight": 10, + "ctSliceHeightMin": 0, + "ctSliceSingle": false, + "ctOutTileSpacing": 1, + "ctOutPower": 100, + "ctOutSpeed": 1000, + "ctOutGroup": true, + "ctOutZColor": false, + "ctOutLayer": false, + "ctOutStack": false, + "ctOutMerged": false, + "ctOriginCenter": true, + "outputInvertX": false, + "outputInvertY": false, + "ctOutKnifeDepth": 1, + "ctOutKnifePasses": 1, + "ctOutKnifeTip": 2 + } + ] +} \ No newline at end of file diff --git a/src2/kiri-dev/wedm/RackRobo.Betta.Wire.V1.json b/src2/kiri-dev/wedm/RackRobo.Betta.Wire.V1.json new file mode 100644 index 00000000..7853ad82 --- /dev/null +++ b/src2/kiri-dev/wedm/RackRobo.Betta.Wire.V1.json @@ -0,0 +1,53 @@ +{ + "mode": "WEDM", + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedWidth": 100, + "bedDepth": 200, + "maxHeight": 50, + "gcodePre": [ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "gcodePost": [ + "M30 ; program end" + ], + "gcodeFExt": "gcode", + "gcodeSpace": true, + "gcodeLaserOn": [ + "M106 S{power}" + ], + "gcodeLaserOff": [ + "M107" + ], + "new": false, + "deviceName": "Rack Robotics Betta Wire V1", + "bedBelt": false, + "bedRound": false, + "originCenter": false, + "fwRetract": false, + "profiles": [ + { + "processName": "wire-edm", + "ctSliceKerf": 0.1, + "ctSliceHeight": 10, + "ctSliceHeightMin": 0, + "ctSliceSingle": false, + "ctOutTileSpacing": 1, + "ctOutPower": 100, + "ctOutSpeed": 1000, + "ctOutGroup": true, + "ctOutZColor": false, + "ctOutLayer": false, + "ctOutStack": false, + "ctOutMerged": false, + "ctOriginCenter": true, + "outputInvertX": false, + "outputInvertY": false, + "ctOutKnifeDepth": 1, + "ctOutKnifePasses": 1, + "ctOutKnifeTip": 2 + } + ] +} \ No newline at end of file diff --git a/src2/kiri-dev/wjet/Any.Generic.WaterJet.json b/src2/kiri-dev/wjet/Any.Generic.WaterJet.json new file mode 100644 index 00000000..5ede1faa --- /dev/null +++ b/src2/kiri-dev/wjet/Any.Generic.WaterJet.json @@ -0,0 +1,21 @@ +{ + "file-ext": "gcode", + "token-space": " ", + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "laser-on":[ + "M106 S{power}" + ], + "laser-off":[ + "M107" + ], + "settings": { + "bed_width": 300, + "bed_depth": 300 + } +} diff --git a/src2/kiri-dev/wjet/HydroBLADE.json b/src2/kiri-dev/wjet/HydroBLADE.json new file mode 100644 index 00000000..74da61c1 --- /dev/null +++ b/src2/kiri-dev/wjet/HydroBLADE.json @@ -0,0 +1,53 @@ +{ + "mode": "WJET", + "internal": 0, + "imageURL": "", + "bedHeight": 2.5, + "bedWidth": 300, + "bedDepth": 300, + "maxHeight": 150, + "gcodePre": [ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "gcodePost": [ + "M30 ; program end" + ], + "gcodeFExt": "gcode", + "gcodeSpace": true, + "gcodeLaserOn": [ + "M106 S{power}" + ], + "gcodeLaserOff": [ + "M107" + ], + "new": false, + "deviceName": "HydroBLADE", + "bedBelt": false, + "bedRound": false, + "originCenter": false, + "fwRetract": false, + "profiles": [ + { + "processName": "waterjet", + "ctSliceKerf": 0.1, + "ctSliceHeight": 10, + "ctSliceHeightMin": 0, + "ctSliceSingle": false, + "ctOutTileSpacing": 1, + "ctOutPower": 100, + "ctOutSpeed": 1000, + "ctOutGroup": true, + "ctOutZColor": false, + "ctOutLayer": false, + "ctOutStack": false, + "ctOutMerged": false, + "ctOriginCenter": true, + "outputInvertX": false, + "outputInvertY": false, + "ctOutKnifeDepth": 1, + "ctOutKnifePasses": 1, + "ctOutKnifeTip": 2 + } + ] +} \ No newline at end of file diff --git a/src2/kiri-mode/cam/anim-2d-be.js b/src2/kiri-mode/cam/anim-2d-be.js new file mode 100644 index 00000000..bc594d30 --- /dev/null +++ b/src2/kiri-mode/cam/anim-2d-be.js @@ -0,0 +1,310 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// WORKER BACK END ANIMATION CODE for 2D + +let stock, center, grid, gridX, gridY, rez; +let path, pathIndex, tool, tools, last, toolID = 1; + +kiri.worker.animate_setup = function(data, send) { + const { settings } = data; + const { process } = settings; + const print = worker.print; + const density = parseInt(settings.controller.animesh) * 1000; + + pathIndex = 0; + path = print.output.flat(); + tools = settings.tools; + stock = settings.stock; + rez = 1/Math.sqrt(density/(stock.x * stock.y)); + + + //destructure arcs into path points + path = path.map(o=> + o.arcPoints + ? [ + ...o.arcPoints.map(point=> + ({ + ...o, + point + }) + ), + o + ] + : o + ) + .flat(); + + const step = rez; + const stepsX = Math.floor(stock.x / step); + const stepsY = Math.floor(stock.y / step); + const { pos, ind, sab } = createGrid(stepsX, stepsY, stock, step, true); + const offset = { + x: process.camOriginCenter ? 0 : stock.x / 2, + y: process.camOriginCenter ? 0 : stock.y / 2, + z: process.camOriginTop ? -stock.z : 0 + } + + grid = pos; + gridX = stepsX; + gridY = stepsY; + + tool = null; + last = null; + animating = false; + animateClear = false; + + center = Object.assign({}, stock.center); + center.z -= stock.z / 2; + + send.data({ mesh_add: { id: 0, ind, offset, sab } }, [ ]); // sab not transferrable + send.data({ mesh_move: { id: 0, pos: center } }); + send.done(); +}; + +kiri.worker.animate = function(data, send) { + renderPause = data.pause || renderPause; + renderSpeed = data.speed || 0; + if (animating) { + return send.done(); + } + renderSteps = data.steps || 1; + renderDone = false; + animating = renderSpeed > 0; + renderPath(send); +}; + +kiri.worker.animate_cleanup = function(data, send) { + if (animating) { + animateClear = true; + } +}; + +function createGrid(stepsX, stepsY, size, step, stock) { + const gridPoints = stepsX * stepsY; + const sab = new SharedArrayBuffer(gridPoints * 3 * 4) + const pos = new Float32Array(sab); + const ind = []; + const ox = size.x / 2; + const oy = size.y / 2; + let ex = stepsX - 1; + let ey = stepsY - 1; + + // initialize grid points + for (let x=0, ai=0; x 0) ind.appendAll([ + (stepsY * x) + (y - 1), + (stepsY * x) + (y ) + ]); + if (x > 0) ind.appendAll([ + (stepsY * (x - 1)) + y, + (stepsY * (x )) + y + ]); + } else { + if (x > 0 && y > 0) { + let v0 = stepsY * (x - 1) + y - 1; + let v1 = stepsY * (x - 0) + y - 1; + let v2 = stepsY * (x - 0) + y; + let v3 = stepsY * (x - 1) + y; + ind.appendAll([ + v0, v1, v2, v0, v2, v3 + ]); + } + } + } + } + + return { pos, ind, sab }; +} + +let animateClear = false; +let animating = false; +let renderDist = 0; +let renderDone = false; +let renderPause = 10; +let renderSteps = 0; +let renderSpeed = 0; +let skipMove = null; +let toolUpdate; +let depth = 0; + +// send latest tool position and progress bar +function renderUpdate(send) { + if (toolUpdate) { + send.data(toolUpdate); + } + send.data({ progress: pathIndex / path.length, id: 0, mesh_update: 1 }); +} + +function renderPath(send) { + if (renderDone) { + return; + } + + if (renderSteps-- === 0) { + animating = false; + renderPath(send); + return; + } + + if (animating === false || animateClear || renderSpeed === 0) { + renderUpdate(send); + renderDone = true; + animating = false; + animateClear = false; + send.done(); + return; + } + + let next = path[pathIndex]; + while (next && next.type === 'laser') { + last = next; + next = path[++pathIndex]; + } + + if (!next) { + animating = false; + renderPath(send); + return; + } + pathIndex++; + + const firstTool = !tool && next.tool; + const toolChange = !firstTool && (tool.getID() !== next.tool.getID()); + if (firstTool || toolChange) { + // on real tool change, go to safe Z first + if (tool && last.point) { + let pos = last.point = { + x: last.point.x, + y: last.point.y, + z: stock.z + }; + send.data({ mesh_move: { toolID, pos }}); + } + updateTool(next.tool, send); + } + + const id = toolID; + const rezstep = rez; + if (last) { + const lp = last.point, np = next.point; + last = next; + // dwell ops have no point + if (!np || !lp) { + return renderPath(send); + } + const dx = np.x - lp.x, dy = np.y - lp.y, dz = np.z - lp.z; + const dist = Math.sqrt(dx*dx + dy*dy + dz*dz); + renderDist += dist; + + // skip moves that are less than grid resolution + if (renderDist < rezstep) { + renderPath(send); + return; + } + + const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz)); + const st = Math.ceil(md / rezstep); + const mx = dx / st, my = dy / st, mz = dz / st; + const moves = []; + for (let i=0, x=lp.x, y=lp.y, z=lp.z; i= renderSpeed) || (depth > 600)) { + renderDist = depth = 0; + renderUpdate(send); + setTimeout(() => { + renderMoves(id, moves, send, index); + }, renderPause); + return; + } + } + depth++; + renderPath(send); +} + +// update stock mesh to reflect tool tip geometry at given XYZ position +function deformMesh(pos, send) { + const prof = tool.profile; + const { size, pix } = tool.profileDim; + const mid = Math.floor(pix / 2); + const rx = Math.floor((pos.x + stock.x / 2 - size / 2 - center.x) / rez); + const ry = Math.floor((pos.y + stock.y / 2 - size / 2 - center.y) / rez); + let upos = 0; + // deform mesh to lowest point on tool profile + for (let i=0, il=prof.length; i < il; ) { + const dx = mid + prof[i++]; + const dy = mid + prof[i++]; + const dz = prof[i++]; + const gx = rx + dx; + const gy = ry + dy; + + if (gx < 0|| gy < 0 || gx > gridX-1 || gy > gridY-1) { + continue; + } + + const gi = gx * gridY + gy; + const iz = gi * 3 + 2; + const cz = grid[iz]; + const tz = tool.pos.z - dz; + if (tz < cz) { + upos++; + grid[iz] = tz; + } + } +} + +function updateTool(toolobj, send) { + if (tool) { + send.data({ mesh_del: toolID }); + } + tool = new CAM.Tool({ tools }, toolobj.getID()); + tool.generateProfile(rez); + const flen = tool.fluteLength() || 15; + const slen = tool.shaftLength() || 15; + // const frad = tool.fluteDiameter() / 2; + const prof = tool.profile; + const { size, pix } = tool.profileDim; + const { pos, ind, sab } = createGrid(pix, pix, {x:size, y:size, z:flen+slen}, rez); + const mid = Math.floor(pix/2); + // deform mesh to fit tool profile + for (let i=0, il=prof.length; i < il; ) { + const dx = mid + prof[i++]; + const dy = mid + prof[i++]; + const dz = prof[i++]; + pos[(dx * pix + dy) * 3 + 2] = -dz; + } + send.data({ mesh_add: { id:++toolID, ind, sab }}); +} diff --git a/src2/kiri-mode/cam/anim-2d-fe.js b/src2/kiri-mode/cam/anim-2d-fe.js new file mode 100644 index 00000000..ce605b58 --- /dev/null +++ b/src2/kiri-mode/cam/anim-2d-fe.js @@ -0,0 +1,309 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// UI FRONT END ANIMATION CODE for 2D + +import { api } from '../../kiri/api.js'; +import { space } from '../../moto/space.js'; + +const { client } = api; + +const asLines = false; +const asPoints = false; + +let meshes = {}, + button = {}, + label = {}, + unitScale = 1, + speedValues = [ 1, 2, 4, 8, 32 ], + speedPauses = [ 30, 20, 10, 5, 0 ], + speedNames = [ "1x", "2x", "4x", "8x", "!!" ], + speedMax = speedValues.length - 1, + speedIndex = 0, + speed, + color = 0, + material, + origin, + posOffset = { x:0, y:0, z:0 }; + +export function animate_clear(api) { + let { anim } = api.ui; + space.platform.showGridBelow(true); + client.animate_cleanup(); + Object.keys(meshes).forEach(id => deleteMesh(id)); + toggleStock(undefined,true,false); + api.uc.setVisible(anim.laba, false); + api.uc.setVisible(anim.vala, false); +} + +export function animate(api, delay) { + let alert = api.alerts.show("building animation"); + let settings = api.conf.get(); + client.animate_setup(settings, data => { + checkMeshCommands(data); + if (!(data && data.mesh_add)) { + return; + } + + let { anim } = api.ui; + Object.assign(button, { + replay: anim.replay, + play: anim.play, + step: anim.step, + pause: anim.pause, + speed: anim.speed, + trans: anim.trans, + model: anim.model, + shade: anim.shade + }); + Object.assign(label, { + progress: anim.progress, + speed: anim.labspd, + x: anim.valx, + y: anim.valy, + z: anim.valz, + }); + + origin = settings.origin; + speedIndex = api.local.getInt('cam.anim.speed') || 0; + updateSpeed(); + setTimeout(step, delay || 0); + + button.replay.onclick = replay; + button.play.onclick = play; + button.step.onclick = step; + button.pause.onclick = pause; + button.speed.onclick = fast; + button.trans.onclick = toggleTrans; + button.model.onclick = toggleModel; + button.shade.onclick = toggleStock; + button.play.style.display = ''; + button.pause.style.display = 'none'; + + api.event.emit('animate', 'CAM'); + api.alerts.hide(alert); + space.platform.showGridBelow(false); + toggleTrans(0,api.local.getBoolean('cam.anim.trans', true)); + toggleModel(0,api.local.getBoolean('cam.anim.model', false)); + toggleStock(0,api.local.getBoolean('cam.anim.stock', false)); + }); +} + +Object.assign(client, { + animate(data, ondone) { + client.send("animate", data, ondone); + }, + + animate_setup(settings, ondone) { + color = settings.controller.dark ? 0x48607B : 0x607FA4; + unitScale = settings.controller.units === 'in' ? 1/25.4 : 1; + let flatShading = true, + transparent = true, + opacity = 0.9, + side = THREE.DoubleSide; + material = new THREE.MeshPhongMaterial({ + flatShading, + transparent, + opacity, + color, + side + }); + add_red_neg_z(material); + client.send("animate_setup", {settings}, ondone); + }, + + animate_cleanup(data, ondone) { + client.send("animate_cleanup", data, ondone); + } +}); + +function meshAdd(id, ind, pos, sab) { + const geo = new THREE.BufferGeometry(); + if (sab) { + // use array buffer shared with worker + pos = new Float32Array(sab); + } + geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + if (ind.length) { + geo.setIndex(new THREE.BufferAttribute(new Uint32Array(ind), 1)); + } + let mesh; + if (asPoints) { + const mat = new THREE.PointsMaterial({ + transparent: true, + opacity: 0.75, + color: 0x888888, + size: 0.3 + }); + mesh = new THREE.Points(geo, mat); + } else if (asLines) { + const mat = new THREE.LineBasicMaterial({ + transparent: true, + opacity: 0.75, + color + }); + mesh = new THREE.LineSegments(geo, mat); + } else { + geo.computeVertexNormals(); + mesh = new THREE.Mesh(geo, material); + mesh.renderOrder = -10; + } + space.world.add(mesh); + meshes[id] = mesh; +} + +function meshUpdates(id) { + const mesh = meshes[id]; + if (!mesh) { + return; // animate cancelled + } + mesh.geometry.attributes.position.needsUpdate = true; + space.update(); +} + +function deleteMesh(id) { + space.world.remove(meshes[id]); + delete meshes[id]; +} + +function toggleModel(ev,bool) { + api.local.toggle('cam.anim.model', bool); + api.widgets.all().forEach(w => w.toggleVisibility(bool)); +} + +function toggleStock(ev,bool,set) { + set !== false && api.local.toggle('cam.anim.stock', bool); + return api.event.emit('cam.stock.toggle', bool ?? undefined); +} + +function toggleTrans(ev,bool) { + bool = api.local.toggle('cam.anim.trans', bool); + material.transparent = bool; + material.needsUpdate = true; +} + +function step() { + updateSpeed(); + client.animate({speed, steps: 1}, handleGridUpdate); +} + +function play(opts) { + const { steps } = opts; + updateSpeed(); + if (steps !== 1) { + button.play.style.display = 'none'; + button.pause.style.display = ''; + } + client.animate({ + speed, + steps: steps || Infinity, + pause: speedPauses[speedIndex] + }, handleGridUpdate); +} + +function fast(opts) { + const { steps } = opts; + updateSpeed(1); + button.play.style.display = 'none'; + button.pause.style.display = ''; + client.animate({ + speed, + steps: steps || Infinity, + pause: speedPauses[speedIndex] + }, handleGridUpdate); +} + +function pause() { + button.play.style.display = ''; + button.pause.style.display = 'none'; + client.animate({speed: 0}, handleGridUpdate); +} + +function handleGridUpdate(data) { + checkMeshCommands(data); + if (data && data.progress) { + label.progress.value = (data.progress * 100).toFixed(1); + } +} + +function updateSpeed(inc = 0) { + if (inc === Infinity) { + speedIndex = speedMax; + } else if (inc > 0) { + speedIndex = (speedIndex + inc) % speedValues.length; + } + api.local.set('cam.anim.speed', speedIndex); + speed = speedValues[speedIndex]; + label.speed.value = speedNames[speedIndex]; +} + +function replay() { + animate_clear(api); + setTimeout(() => { + animate(api, 50); + }, 250); +} + +function checkMeshCommands(data) { + if (!data) { + return; + } + if (data.mesh_add) { + const { id, ind, pos, offset, sab } = data.mesh_add; + meshAdd(id, ind, pos, sab); + space.refresh(); + if (offset) { + posOffset = offset; + } + } + if (data.mesh_del) { + deleteMesh(data.mesh_del); + } + if (data.mesh_move) { + const { id, pos } = data.mesh_move; + const mesh = meshes[id]; + if (mesh) { + mesh.position.x = pos.x; + mesh.position.y = pos.y; + mesh.position.z = pos.z; + space.update(); + } + label.x.value = (pos.x - origin.x).toFixed(2); + label.y.value = (pos.y + origin.y).toFixed(2); + label.z.value = (pos.z - origin.z).toFixed(2); + } + if (data.mesh_update) { + meshUpdates(data.id); + } +} + +// SHADER: tint points below z=0 with red +function add_red_neg_z(material) { + material.onBeforeCompile = (shader) => { + shader.vertexShader = shader.vertexShader.replace( + `#include `, + ` + #include + vWorldPosition = vec3(transformed); + ` + ); + + shader.vertexShader = ` + varying vec3 vWorldPosition; + ` + shader.vertexShader; + + shader.fragmentShader = ` + varying vec3 vWorldPosition; + ` + shader.fragmentShader; + + shader.fragmentShader = shader.fragmentShader.replace( + `#include `, + ` + #include + if (vWorldPosition.z < 0.0) { + gl_FragColor.rgb += vec3(0.5, 0.0, 0.0); // Add red tint + } + ` + ); + }; + return material; +} diff --git a/src2/kiri-mode/cam/anim-3d-be.js b/src2/kiri-mode/cam/anim-3d-be.js new file mode 100644 index 00000000..d634bf22 --- /dev/null +++ b/src2/kiri-mode/cam/anim-3d-be.js @@ -0,0 +1,391 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// WORKER BACK END ANIMATION CODE for 3D (indexed) + +import { CSG } from '../../geo/csg.js'; + +let nextMeshID = 1; +let stock, center, rez; +let path, pathIndex, tool, tools, last; + +let stockZ; +let stockIndexMsg = false; +let stockSlices; +let stockIndex; +let startTime; + +let toolID = -1; +let toolMesh; +let toolRadius; +let toolUpdateMsg; + +let animateClear = false; +let animating = false; +let moveDist = 0; +let renderDist = 0; +let renderDone = false; +let renderPause = 10; +let renderSteps = 0; +let renderSpeed = 0; +let indexCount = 0; +let updates = 0; + +kiri.worker.animate_setup2 = function (data, send) { + const { settings } = data; + const { process } = settings; + const print = worker.print; + const density = parseInt(settings.controller.animesh) * 1000; + const isIndexed = process.camStockIndexed; + + pathIndex = 0; + path = print.output.flat(); + tools = settings.tools; + stock = settings.stock; + rez = 1 / Math.sqrt(density / (stock.x * stock.y)); + + tool = null; + last = null; + animating = false; + animateClear = false; + stockIndex = 0; + indexCount = 0; + startTime = 0; + updates = 0; + stockZ = isIndexed ? 0 : stock.z; + + stockSlices = []; + const { x, y, z } = stock; + const sliceCount = parseInt(settings.controller.animesh || 2000) / 100; + const sliceWidth = stock.x / sliceCount; + for (let i = 0; i < sliceCount; i++) { + let xmin = -(x / 2) + (i * sliceWidth) + sliceWidth / 2; + let slice = new Stock(sliceWidth, y, z).translate(xmin, 0, 0); + stockSlices.push(slice); + slice.updateMesh([]); + slice.send(send); + // send({ mesh_move: { id: slice.id, pos: { x:0, y:0, z: stock.z/2} } }); + } + + send.done(); +}; + +kiri.worker.animate2 = function (data, send) { + renderPause = data.pause || renderPause; + renderSpeed = data.speed || 0; + if (animating) { + return send.done(); + } + renderSteps = data.steps || 1; + renderDone = false; + animating = renderSpeed > 0; + startTime = startTime || Date.now(); + renderPath(send); +}; + +kiri.worker.animate_cleanup2 = function (data, send) { + if (animating) { + animateClear = true; + } +}; + +function renderPath(send) { + if (renderDone) { + return; + } + + if (renderSteps-- === 0) { + animating = false; + renderPath(send); + return; + } + + if (animating === false || animateClear || renderSpeed === 0) { + renderUpdate(send); + renderDone = true; + animating = false; + animateClear = false; + send.done(); + return; + } + + let next = path[pathIndex]; + while (next && next.type === 'laser') { + last = next; + next = path[++pathIndex]; + } + + if (!next) { + console.log('animation completed in ', ((Date.now() - startTime) / 1000).round(2)); + animating = false; + renderPath(send); + return; + } + pathIndex++; + // console.log(next.point.z); + + if (next.tool && (!tool || tool.getID() !== next.tool.getID())) { + // on real tool change, go to safe Z first + if (tool && last.point) { + let pos = last.point = { + x: last.point.x, + y: last.point.y, + z: stock.z + }; + toolMove(pos); + send.data(toolUpdateMsg); + } + toolUpdate(next.tool.getID(), send); + } + + const id = toolID; + const rezstep = rez * 2; + if (last) { + const lp = last.point, np = next.point; + last = next; + // dwell ops have no point + if (!np || !lp) { + return renderPath(send); + } + let dx = np.x - lp.x, + dy = np.y - lp.y, + dz = np.z - lp.z, + da = Math.abs((np.a || 0) - (lp.a || 0)), + dr = (da / 360) * (2 * Math.PI * Math.max(np.z, lp.z)), + dist = Math.sqrt(dx * dx + dy * dy + dz * dz + dr * dr); + + moveDist += dist; + + // skip moves that are less than grid resolution + if (moveDist < rezstep) { + // console.log('skip', moveDist, rezstep, next); + renderPath(send); + return; + } + + const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz), dr); + const st = Math.ceil(md / rezstep); + const mx = dx / st, my = dy / st, mz = dz / st; + // const sd = Math.sqrt(mx*mx + my*my + mz*mz + dr*dr); + const sd = Math.sqrt(mx * mx + my * my + Math.min(1, mz * mz) + dr * dr); + const moves = []; + for (let i = 0, x = lp.x, y = lp.y, z = lp.z; i < st; i++) { + moves.push({ x, y, z, a: lp.a, md: sd, dx, dy, dz }); + x += mx; + y += my; + z += mz; + } + moveDist = 0; + moves.push({ ...next.point, md: sd }); + renderMoves(id, moves, send); + } else { + last = next; + if (tool) { + toolMove(next.point); + } + renderPath(send); + } +} + +function renderMoves(id, moves, send, seed = 0) { + for (let index = seed; index < moves.length; index++) { + const pos = moves[index]; + if (!pos) { + throw `no pos @ ${index} of ${moves.length}`; + } + const { dx, dy, dz } = pos; + toolMove(pos); + // console.log('renderMoves', {id, moves, seed}); + let subs = 0; + if (dx || dy || dz < 0) + for (let slice of stockSlices) { + if (slice.bounds.intersectsBox(toolMesh.bounds)) { + slice.subtractTool(toolMesh); + subs++; + } + } + // console.log({ index, subs }); + renderDist += pos.md; + // pause renderer at specified offsets + if (renderSpeed && renderDist >= renderSpeed) { + renderDist = 0; + renderUpdate(send); + setTimeout(() => { + renderMoves(id, moves, send, index + 1); + }, renderPause); + return; + } + } + renderPath(send); +} + +// send latest tool position and progress bar +function renderUpdate(send) { + const updated = [] + for (let slice of stockSlices) { + slice.updateMesh(updated); + } + for (let slice of updated) { + slice.send(send); + } + if (toolUpdateMsg) { + send.data(toolUpdateMsg); + } + if (stockIndexMsg) { + send.data({ stock_index: stockIndex }); + for (let slice of stockSlices) { + send.data({ mesh_index: { id: slice.id, index: -stockIndex } }); + } + stockIndexMsg = false; + } + send.data({ progress: pathIndex / path.length }); + updates++; +} + +// move tool mesh animation space, update client +function toolMove(pos) { + toolUpdateMsg = { mesh_move: { id: toolID, pos: { x: pos.x, y: pos.y, z: pos.z } } }; + if (toolMesh.mesh) { + toolMesh.mesh.delete(); + } + toolMesh.mesh = toolMesh.root.translate(pos.x, pos.y, pos.z); + if (pos.a !== undefined) { + let tmp = toolMesh.mesh.rotate([pos.a, 0, 0]); + toolMesh.mesh.delete(); + toolMesh.mesh = tmp; + if (pos.a !== stockIndex) { + stockIndexMsg = true; + stockIndex = pos.a; + } + } + toolMesh.bounds = CSG.toBox3(toolMesh.mesh); +} + +// delete old tool mesh, generate tool mesh, send to client +function toolUpdate(toolid, send) { + if (tool) { + send.data({ mesh_del: toolID }); + } + tool = new CAM.Tool({ tools }, toolid); + const Instance = CSG.Instance(); + const slen = tool.shaftLength() || 15; + const srad = tool.shaftDiameter() / 2; + const flen = tool.fluteLength() || 15; + const frad = toolRadius = tool.fluteDiameter() / 2; + const tlen = slen + flen; // total tool length + let { cylinder, sphere } = Instance.Manifold; + let mesh; + if (tool.isBallMill()) { + mesh = cylinder(tlen - frad * 2, frad, frad, 20, true) + .add(sphere(frad, 20).translate(0, 0, -(tlen - frad * 2) / 2)) + .add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen / 2)); + } else if (tool.isTaperMill()) { + const trad = Math.max(tool.tipDiameter() / 2, 0.001); + mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2) + .add(cylinder(flen, trad, frad, 20, true).translate(0, 0, -flen / 2)); + } else { + mesh = cylinder(tlen, frad, frad, 20, true) + .add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen / 2)); + } + mesh = mesh.translate(0, 0, (tlen - stockZ) / 2); + const raw = mesh.getMesh(); + const vertex = raw.vertProperties; + const index = raw.triVerts; + toolMesh = { root: mesh, index, vertex, bounds: CSG.toBox3(mesh) }; + send.data({ mesh_add: { id: --toolID, ind: index, pos: vertex } }); +} + +class Stock { + constructor(x, y, z) { + this.id = nextMeshID++; + this.vbuf = undefined; + this.ibuf = undefined; + this.ilen = 0; + this.sends = 0; + this.newbuf = true; + this.subtracts = 0; + this.mesh = CSG.Instance().Manifold.cube([x, y, z], true); + } + + send(send) { + const newbuf = this.newbuf; + const action = this.sends++ === 0 ? 'mesh_add' : 'mesh_update'; + send.data({ + [action]: { + id: this.id, + ind: newbuf ? this.ibuf : undefined, + pos: newbuf ? this.vbuf : undefined, + ilen: this.ilen, + plen: this.plen + } + }); + this.newbuf = false; + this.sends++; + // console.log({ send: this.id, newbuf, action }); + } + + translate(x, y, z) { + // console.log({ translate: this.id, x, y, z }); + const oldmesh = this.mesh; + this.mesh = this.mesh.translate(x, y, z); + this.bounds = CSG.toBox3(this.mesh); + oldmesh.delete(); + return this; + } + + updateMesh(updates) { + if (this.sends > 0 && this.subtracts === 0) { + return; + } + const subs = this.subtracts; + this.subtracts = 0; + let start = Date.now(); + let mesh = this.mesh.getMesh(); + this.sharedVertexBuffer(mesh.numVert * 3); + this.sharedIndexBuffer(mesh.numTri * 3); + this.vbuf.set(mesh.vertProperties); + this.ibuf.set(mesh.triVerts); + let sub = Date.now(); + updates.push(this); + if (false) console.log({ + update: this.id, + time: sub - start, + subs, + mssub: ((sub - start) / subs).round(3) + }); + } + + subtractTool(toolMesh) { + const oldmesh = this.mesh; + this.mesh = this.mesh.subtract(toolMesh.mesh); + oldmesh.delete(); + this.subtracts++; + } + + sharedVertexBuffer(size) { + const old = this.vbuf; + this.plen = size; + if (old && old.length >= size) { + // console.log({svb: this.id, reuse: size, old: old.length}); + return old; + } + // let buf = new Float32Array(new SharedArrayBuffer(size * 4)); + let buf = new Float32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024)); + // console.log({new_svb: this.id, size, buf}); + this.newbuf = true; + return this.vbuf = buf; + } + + sharedIndexBuffer(size) { + const old = this.ibuf; + this.ilen = size; + if (old && old.length >= size) { + // console.log({sib: this.id, reuse: size, old: old.length}); + return old; + } + // let buf = new Uint32Array(new SharedArrayBuffer(size * 4)); + let buf = new Uint32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024)); + // console.log({new_sib: this.id, size, buf}); + this.newbuf = true; + return this.ibuf = buf; + } +} diff --git a/src2/kiri-mode/cam/anim-3d-fe.js b/src2/kiri-mode/cam/anim-3d-fe.js new file mode 100644 index 00000000..b923f71a --- /dev/null +++ b/src2/kiri-mode/cam/anim-3d-fe.js @@ -0,0 +1,253 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// UI FRONT END ANIMATION CODE for 3D (indexed) + +import { api } from '../../kiri/api.js'; +import { space } from '../../moto/space.js'; + +const { client } = api; + +let meshes = {}, + button = {}, + label = {}, + material, + speedPauses = [0, 0, 0, 0, 0], + speedValues = [1, 2, 4, 8, 32], + speedNames = ["1x", "2x", "4x", "8x", "!!"], + speedMax = speedValues.length - 1, + speedIndex = 0, + speed, + origin, + color = 0; + +export function animate_clear2(api) { + let { anim } = api.ui; + client.animate_cleanup2(); + Object.keys(meshes).forEach(id => deleteMesh(id)); + api.widgets.setAxisIndex(0); + api.uc.setVisible(anim.laba, true); + api.uc.setVisible(anim.vala, true); + anim.vala.value = "0.0"; +} + +export function animate2(api, delay) { + let alert = api.alerts.show("building animation"); + let settings = api.conf.get(); + client.animate_setup2(settings, data => { + handleUpdate(data); + if (data) { + return; + } + + let { anim } = api.ui; + Object.assign(button, { + replay: anim.replay, + play: anim.play, + step: anim.step, + pause: anim.pause, + speed: anim.speed, + trans: anim.trans, + model: anim.model, + shade: anim.shade + }); + Object.assign(label, { + progress: anim.progress, + speed: anim.labspd, + x: anim.valx, + y: anim.valy, + z: anim.valz, + a: anim.vala + }); + + updateSpeed(0); + setTimeout(step, delay || 0); + toggleTrans(undefined, false); + origin = settings.origin; + + button.replay.onclick = replay; + button.play.onclick = play; + button.step.onclick = step; + button.pause.onclick = pause; + button.speed.onclick = fast; + button.trans.onclick = toggleTrans; + button.model.onclick = toggleModel; + button.shade.onclick = toggleStock; + button.play.style.display = ''; + button.pause.style.display = 'none'; + + api.event.emit('animate', 'CAM'); + api.alerts.hide(alert); + }); +} + +Object.assign(client, { + animate2(data, ondone) { + client.send("animate2", data, ondone); + }, + + animate_setup2(settings, ondone) { + color = settings.controller.dark ? 0x888888 : 0; + material = new THREE.MeshMatcapMaterial({ + flatShading: true, + transparent: false, + opacity: 0.9, + color: 0x888888, + side: THREE.DoubleSide + }); + client.send("animate_setup2", { settings }, ondone); + }, + + animate_cleanup2(data, ondone) { + client.send("animate_cleanup2", data, ondone); + } +}); + +function meshAdd(id, ind, pos, ilen, plen) { + const geo = new THREE.BufferGeometry(); + const pa = plen ? pos.subarray(0, plen * 3) : pos; + const ia = ilen ? ind.subarray(0, ilen) : ind; + geo.setAttribute('position', new THREE.BufferAttribute(pa, 3)); + geo.setIndex(new THREE.BufferAttribute(ia, 1)); + const mesh = new THREE.Mesh(geo, material); + mesh.pos = pos; + mesh.ind = ind; + space.world.add(mesh); + meshes[id] = mesh; +} + +function meshUpdate(id, ind, pos, ilen, plen) { + const mesh = meshes[id]; + if (!mesh) { + return; // animate cancelled + } + const geo = mesh.geometry; + mesh.pos = pos || mesh.pos; + mesh.ind = ind || mesh.ind; + geo.setAttribute('position', new THREE.BufferAttribute(mesh.pos.subarray(0, plen * 3), 3)); + geo.setIndex(new THREE.BufferAttribute(mesh.ind.subarray(0, ilen), 1)); + geo.attributes.position.needsUpdate = true; + geo.index.needsUpdate = true; + space.update(); +} + +function deleteMesh(id) { + space.world.remove(meshes[id]); + delete meshes[id]; +} + +function toggleModel(ev, bool) { + api.local.toggle('cam.anim.model', bool); + api.widgets.all().forEach(w => w.toggleVisibility(bool)); +} + +function toggleStock(ev, bool, set) { + set !== false && api.local.toggle('cam.anim.stock', bool); + return api.event.emit('cam.stock.toggle', bool ?? undefined); +} + +function toggleTrans(ev, bool) { + bool = api.local.toggle('cam.anim.trans', bool); + material.transparent = bool; + material.needsUpdate = true; +} + +function step() { + updateSpeed(); + client.animate2({ speed, steps: 1 }, handleUpdate); +} + +function play(opts) { + const { steps } = opts; + updateSpeed(); + if (steps !== 1) { + button.play.style.display = 'none'; + button.pause.style.display = ''; + } + client.animate2({ + speed, + steps: steps || Infinity, + pause: speedPauses[speedIndex] + }, handleUpdate); +} + +function fast(opts) { + const { steps } = opts; + updateSpeed(1); + button.play.style.display = 'none'; + button.pause.style.display = ''; + client.animate2({ + speed, + steps: steps || Infinity, + pause: speedPauses[speedIndex] + }, handleUpdate); +} + +function pause() { + button.play.style.display = ''; + button.pause.style.display = 'none'; + client.animate2({ speed: 0 }, handleUpdate); +} + +function handleUpdate(data) { + if (!data) { + return; + } + if (data.mesh_add) { + const { id, ind, pos, ilen, plen } = data.mesh_add; + meshAdd(id, ind, pos, ilen, plen); + space.refresh(); + } + if (data.mesh_del) { + deleteMesh(data.mesh_del); + } + if (data.mesh_move) { + const { id, pos } = data.mesh_move; + const mesh = meshes[id]; + if (mesh) { + mesh.position.x = pos.x; + mesh.position.y = pos.y; + mesh.position.z = pos.z; + space.refresh(); + } + label.x.value = (pos.x - origin.x).toFixed(2); + label.y.value = (pos.y + origin.y).toFixed(2); + label.z.value = (pos.z - origin.z).toFixed(2); + } + if (data.stock_index !== undefined) { + api.widgets.setAxisIndex(data.stock_index); + label.a.value = -data.stock_index.toFixed(1); + } + if (data.mesh_index) { + const { id, index } = data.mesh_index; + const mesh = meshes[id]; + if (mesh) { + mesh.rotation.x = (Math.PI / 180) * index; + space.refresh(); + } + } + if (data.mesh_update) { + const { id, ind, pos, ilen, plen } = data.mesh_update; + meshUpdate(id, ind, pos, ilen, plen); + } + if (data && data.progress) { + label.progress.value = (data.progress * 100).toFixed(1); + } +} + +function updateSpeed(inc = 0) { + if (inc === Infinity) { + speedIndex = speedMax; + } else if (inc > 0) { + speedIndex = (speedIndex + inc) % speedValues.length; + } + api.local.set('cam.anim.speed', speedIndex); + speed = speedValues[speedIndex]; + label.speed.value = speedNames[speedIndex]; +} + +function replay() { + animate_clear2(api); + setTimeout(() => { + animate2(api, 50); + }, 250); +} diff --git a/src2/kiri-mode/cam/client.js b/src2/kiri-mode/cam/client.js new file mode 100644 index 00000000..5279bf2f --- /dev/null +++ b/src2/kiri-mode/cam/client.js @@ -0,0 +1,2305 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $, h } from '../../moto/webui.js'; +import { api } from '../../kiri/api.js'; +import { load } from '../../load/file.js'; +import { addbox, delbox } from '../../kiri/boxes.js'; +import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js'; +import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js'; +import { space as SPACE } from '../../moto/space.js'; +import { Layers } from '../../kiri/layers.js'; +import { Stack } from '../../kiri/stack.js'; +import { CAM } from './driver-fe.js'; + +const { alerts, conf, noop, space } = api; + +const DEG2RAD = Math.PI / 180; +const RAD2DEG = 180 / Math.PI; +const hasSharedArrays = self.SharedArrayBuffer ? true : false; + +let isAnimate, + isArrange, + isPreview, + isCamMode, + isIndexed, + isParsed, + camStock, + camZTop, + camZBottom, + current, + currentIndex, + flipping, + poppedRec, + hoveredOp, + lastMode, + { MODES, VIEWS, STACKS } = api.const, + LANG = api.language.current, + WIDGETS = api.widgets, + UI = api.ui, + UC = api.uc, + MCAM = MODES.CAM; + +let zaxis = { x: 0, y: 0, z: 1 }, + popOp = {}, + animVer = 0, + seed = Date.now(), + func = { + hover: noop, + hoverUp: noop + }; + +function animFn() { + return [{ + animate: anim_2d, + animate_clear: anim_2d_clear + },{ + animate: anim_3d, + animate_clear: anim_3d_clear + }][animVer]; +} + +function updateIndex() { + let oplist = current.process.ops; + if (!(isCamMode && oplist) || lastMode === VIEWS.ANIMATE) { + return; + } + let index = 0; + for (let op of oplist) { + if (op.type === '|') { + break; + } + if (op.type === 'index' && !op.disabled) { + if (op.absolute) { + index = op.degrees + } else { + index += op.degrees; + } + } + } + WIDGETS.setAxisIndex(isPreview || !isIndexed ? 0 : -index); + currentIndex = isIndexed && !isPreview ? index * DEG2RAD : 0; +} + +function updateAxisMode(refresh) { + const { camStockIndexGrid, camStockIndexed } = current.process; + let newIndexed = camStockIndexed; + let changed = refresh || isIndexed !== newIndexed; + isIndexed = newIndexed; + if (!isIndexed || !isCamMode) { + WIDGETS.setAxisIndex(0); + } + if (!isCamMode) { + return; + } + if (isIndexed) { + current.process.camZAnchor = "middle"; + } + animVer = isIndexed ? 1 : 0; + SPACE.platform.setVisible(!isIndexed); + SPACE.platform.showGrid2(!isIndexed || camStockIndexGrid); + const showIndexed = isIndexed ? '' : 'none'; + const showNonIndexed = isIndexed ? 'none' : ''; + $('cam-index').style.display = showIndexed; + $('cam-lathe').style.display = showIndexed; + $('cam-flip').style.display = showNonIndexed; + $('cam-reg').style.display = showNonIndexed; + if (!changed) { + return; + } + WIDGETS.setIndexed(isIndexed ? true : false); + api.platform.update_bounds(); + // add or remove clock op depending on indexing + const cp = current.process; + if (!cp.ops) { + return; + } + const clockOp = cp.ops.filter(op => op.type === '|')[0]; + if (!clockOp) { + func.opAdd(popOp['|'].new()); + } else { + func.opRender(); + } + updateStock(); +} + +// create custom gcode editor function +function gcodeEditor(label, field) { + return function() { + func.opGCode(label, field); + } +} + +function mirrorTabs(widget) { + let tabs = api.widgets.annotate(widget.id).tab || []; + tabs.forEach(rec => { + let { id, pos, rot } = rec; + let tab = widget.tabs[id]; + let e = new THREE.Euler().setFromQuaternion(rot); + e._z = Math.PI - e._z; + let { _x, _y, _z, _w } = rec.rot; + let or = new THREE.Quaternion(_x, _y, _z, _w); + let nr = new THREE.Quaternion().setFromEuler(e); + let ra = or.angleTo(nr); + // console.log({or, nr, ra}); + rec.rot = nr; + // let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(0,0,e._z)); + // tab.box.geometry.applyMatrix4(m4); + tab.box.position.x = pos.x = -pos.x; + }); + SPACE.update(); +} + +function rotateTabs(widget, x, y, z) { + let tabs = api.widgets.annotate(widget.id).tab || []; + tabs.forEach(rec => { + let { id, pos, rot } = rec; + if (!Array.isArray(rot)) { + rot = rot.toArray(); + } + let coff = widget.track.center; + let tab = widget.tabs[id]; + let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0)); + // update position vector + let vc = new THREE.Vector3(pos.x, pos.y, pos.z).applyMatrix4(m4); + // update rotation quaternion + let [ rx, ry, rz, rw ] = rot; + rec.rot = new THREE.Quaternion().multiplyQuaternions( + new THREE.Quaternion(rx, ry, rz, rw), + new THREE.Quaternion().setFromRotationMatrix(m4) + ).toArray(); + tab.box.geometry.applyMatrix4(m4); + tab.box.position.x = pos.x = vc.x - coff.dx; + tab.box.position.y = pos.y = vc.y - coff.dy; + tab.box.position.z = pos.z = vc.z; + }); + SPACE.update(); +} + +function hasIndexing() { + return isIndexed; +} + +function hasSpindle() { + return current.device.spindleMax > 0; +} + +function zTop() { + return api.conf.get().process.camZTop > 0; +} + +function zBottom() { + return api.conf.get().process.camZBottom > 0; +} + +func.opAddLaserOn = () => { + func.opAdd(popOp['laser on'].new()); +}; + +func.opAddLaserOff = () => { + func.opAdd(popOp['laser off'].new()); +}; + +func.opAddGCode = () => { + func.opAdd(popOp.gcode.new()); +}; + +func.opAddIndex = () => { + func.opAdd(popOp.index.new()); +}; + +func.opAddLevel = () => { + func.opAdd(popOp.level.new()); +}; + +func.opAddRough = () => { + func.opAdd(popOp.rough.new()); +}; + +func.opAddOutline = () => { + func.opAdd(popOp.outline.new()); +}; + +func.opAddPocket = () => { + func.traceDone(); + func.surfaceDone(); + let rec = popOp.pocket.new(); + rec.surfaces = { /* widget.id: [ faces... ] */ }; + func.opAdd(rec); +}; + +func.opAddContour = (axis) => { + let rec = popOp.contour.new(); + rec.axis = axis.toUpperCase(); + func.opAdd(rec); +}; + +func.opAddLathe = (axis) => { + let rec = popOp.lathe.new(); + rec.axis = axis.toUpperCase(); + func.opAdd(rec); +}; + +func.opAddTrace = () => { + let rec = popOp.trace.new(); + rec.areas = { /* widget.id: [ polygons... ] */ }; + func.opAdd(rec); +}; + +func.opAddDrill = () => { + let rec = popOp.drill.new(); + rec.drills = { }; + func.opAdd(rec); +}; + +func.opAddRegister = (axis, points) => { + let rec = popOp.register.new(); + rec.axis = axis.toUpperCase(); + rec.points = points; + func.opAdd(rec); +}; + +func.opAddFlip = () => { + func.opAdd(popOp.flip.new()); +}; + +func.opGCode = (label, field = 'gcode') => { + api.dialog.show('any'); + const { c_gcode } = h.bind( + $('mod-any'), h.div({ id: "camop_dialog" }, [ + h.label(label || 'custom gcode operation'), + h.textarea({ id: "c_gcode", rows: 15, cols: 50 }), + h.button({ _: 'done', onclick: () => { + api.dialog.hide(); + api.conf.save(); + } }) + ]) + ); + let av = poppedRec[field] || []; + c_gcode.value = typeof(av) === 'string' ? av : av.join('\n'); + c_gcode.onkeyup = (el) => { + poppedRec[field] = c_gcode.value.trim().split('\n'); + }; + c_gcode.focus(); +}; + +func.tabHover = function(data) { + delbox('tabb'); + const { int, type, point } = data; + const object = int ? int.object : null; + const tab = int ? object.tab : null; + if (lastTab) { + lastTab.box.material.color.r = 0; + lastTab = null; + } + if (tab) { + tab.box.material.color.r = 0.5; + lastTab = tab; + return; + } + if (type !== 'widget') { + iw = null; + return; + } + let n = int.face.normal; + iw = int.object.widget; + ic = int.point; + // only near vertical faces + // if (Math.abs(n.z) > 0.3) { + // return; + // } + showTab = createTabBox(iw, ic, n); +}; + +func.tabHoverUp = function(int) { + delbox('tabb'); + if (lastTab) { + const {widget, box, id} = lastTab; + widget.adds.remove(box); + widget.mesh.remove(box); + delete widget.tabs[id]; + let ta = api.widgets.annotate(widget.id).tab; + let ix = 0; + ta.forEach((rec,i) => { + if (rec.id === id) { + ix = i; + } + }); + ta.splice(ix,1); + api.conf.save(); + widget.saveState(); + return; + } + if (!iw) return; + let ip = iw.track.pos; + let wa = api.widgets.annotate(iw.id); + let wt = (wa.tab = wa.tab || []); + let pos = { + x: showTab.pos.x - ip.x, + y: -showTab.pos.z - ip.y, + z: showTab.stock.z ? + showTab.pos.y + ip.z + (isIndexed ? 0 : iw.track.tzoff) : + showTab.dim.z/2, + } + let id = Date.now(); + let { dim, rot } = showTab; + let rec = { pos, dim, rot, id }; + wt.push(Object.clone(rec)); + addWidgetTab(iw, rec); + api.conf.save(); + iw.saveState(); +}; + +// SURFACE FUNCS +let surfaceOn = false, lastWidget; +func.surfaceAdd = (ev) => { + if (surfaceOn) { + return func.surfaceDone(); + } + func.clearPops(); + alert = api.show.alert("analyzing surfaces...", 1000); + let surfaces = poppedRec.surfaces; + let radians = poppedRec.follow * DEG2RAD; + CAM.surface_prep(currentIndex * RAD2DEG, () => { + api.hide.alert(alert); + alert = api.show.alert("[esc] cancels surface selection"); + for (let [wid, arr] of Object.entries(surfaces)) { + let widget = api.widgets.forid(wid); + if (widget && arr.length) + for (let faceid of arr) { + CAM.surface_toggle(widget, faceid, radians, faceids => { + // surfaces[widget.id] = faceids; + }); + } + } + }); + surfaceOn = hoveredOp; + surfaceOn.classList.add("editing"); + api.feature.on_mouse_up = (obj, ev) => { + let { face } = obj; + let min = Math.min(face.a, face.b, face.c); + let faceid = min / 3; + let widget = lastWidget = obj.object.widget; + CAM.surface_toggle(widget, faceid, radians, faceids => { + surfaces[widget.id] = faceids; + }); + }; +}; + +func.surfaceDone = () => { + if (!(surfaceOn && poppedRec && poppedRec.surfaces)) { + return; + } + let surfaces = poppedRec.surfaces; + for (let wid of Object.keys(surfaces)) { + let widget = api.widgets.forid(wid); + if (widget) { + CAM.surface_clear(widget); + } else { + delete surfaces[wid]; + } + } + api.hide.alert(alert); + api.feature.on_mouse_up = undefined; + surfaceOn.classList.remove("editing"); + surfaceOn = false; +}; + +// TRACE FUNCS +let traceOn = false, lastTrace; + +func.traceAdd = (ev) => { + if (traceOn) { + return func.traceDone(); + } + func.clearPops(); + alert = api.show.alert("analyzing parts...", 1000); + traceOn = hoveredOp; + traceOn.classList.add("editing"); + api.feature.hover = true; + api.feature.hoverAdds = true; + func.hover = func.traceHover; + func.hoverUp = func.traceHoverUp; + CAM.traces((ids) => { + api.hide.alert(alert); + alert = api.show.alert("[esc] cancels trace editing"); + kiri.api.widgets.for(widget => { + if (ids.indexOf(widget.id) >= 0) { + unselectTraces(widget, true); + widget.trace_stack = null; + } + if (widget.trace_stack) { + widget.adds.appendAll(widget.trace_stack.meshes); + widget.trace_stack.show(); + return; + } + let areas = (poppedRec.areas[widget.id] || []); + let stack = new Stack(widget.mesh); + widget.trace_stack = stack; + widget.traces.forEach(poly => { + let match = areas.filter(arr => poly.matches(arr)); + let layers = new Layers(); + layers.setLayer("trace", {line: 0xaaaa55, fat:4, order:-10}, false).addPoly(poly); + stack.addLayers(layers); + stack.new_meshes.forEach(mesh => { + mesh.trace = {widget, poly}; + // ensure trace poly singleton from matches + if (match.length > 0) { + poly._trace = match[0]; + } else { + poly._trace = poly.toArray(); + } + }); + widget.adds.appendAll(stack.new_meshes); + // console.log(widget) + }); + }); + // ensure appropriate traces are toggled matching current record + kiri.api.widgets.for(widget => { + widget.setVisualState({ opacity: 0.25 }); + let areas = (poppedRec.areas[widget.id] || []); + let stack = widget.trace_stack; + stack.meshes.forEach(mesh => { + let { poly } = mesh.trace; + let match = areas.filter(arr => poly.matches(arr)); + if (match.length > 0) { + if (!mesh.selected) { + func.traceToggle(mesh, true); + } + } else if (mesh.selected) { + func.traceToggle(mesh, true); + } + }); + }); + }, poppedRec.select === 'lines'); +}; + +func.traceDone = () => { + if (!traceOn) { + return; + } + func.unpop(); + traceOn.classList.remove("editing"); + traceOn = false; + kiri.api.widgets.opacity(1); + api.hide.alert(alert); + api.feature.hover = false; + api.feature.hoverAdds = false; + kiri.api.widgets.for(widget => { + widget.restoreVisualState(); + if (widget.trace_stack) { + widget.trace_stack.hide(); + widget.adds.removeAll(widget.trace_stack.meshes); + } + }); +}; + +func.clearPops = () => { + if (func.unpop) func.unpop(); + func.tabDone(); + func.traceDone(); + func.surfaceDone(); + func.selectHolesDone(); +}; + +func.traceHover = function(data) { + if (lastTrace) { + let { color, colorSave } = lastTrace.material[0] || lastTrace.material; + color.r = colorSave.r; + color.g = colorSave.g; + color.b = colorSave.b; + } + lastTrace = null; + if (data.type === 'platform') { + return; + } + if (!data.int.object.trace) { + return; + } + lastTrace = data.int.object; + if (lastTrace.selected) { + let event = data.event; + let target = event.target; + let { clientX, clientY } = event; + let { offsetWidth, offsetHeight } = target; + } + let material = lastTrace.material[0] || lastTrace.material; + let color = material.color; + material.colorSave = color.clone(); + color.setHex(isDark() ? 0x0066ff : 0x0000ff); +}; + +func.traceHoverUp = function(int, ev) { + if (!int) return; + let { object } = int; + func.traceToggle(object); + if (ev.metaKey || ev.ctrlKey) { + let { selected } = object; + let { widget, poly } = object.trace; + let avgZ = poly.avgZ(); + for (let add of widget.adds) { + if (add.trace && add.selected !== selected && add.trace.poly.onZ(avgZ)) { + func.traceToggle(add); + } + } + } +}; + +func.traceToggle = function(obj, skip) { + let material = obj.material[0] || obj.material; + if (!material) return; + let { color, colorSave } = material; + let { widget, poly } = obj.trace; + let areas = poppedRec.areas; + if (!areas) { + return; + } + let wlist = areas[widget.id] = areas[widget.id] || []; + obj.selected = !obj.selected; + if (!colorSave) { + colorSave = material.colorSave = color.clone(); + } + if (obj.selected) { + color.setHex(isDark() ? 0xdd0011 : 0xff0033); + colorSave.r = color.r; + colorSave.g = color.g; + colorSave.b = color.b; + if (!skip) wlist.push(poly._trace); + } else { + color.setHex(0xaaaa55); + colorSave.setHex(0xaaaa55); + if (!skip) wlist.remove(poly._trace); + } + api.conf.save(); +}; + +let holeSelOn = false, lastSelHoles; + +/** + * Client side function to select holes in widgets for CAM operations. + * If holes have already been analyzed, they are displayed immediately. + * Otherwise, the CAM server is queried and the results are cached. + * @param {boolean} individual - select all holes, false to only select + * holes of tool diameter. + */ +func.selectHoles = async function(individual){ + // console.log("client individual selected",individual) + if (holeSelOn) { + return func.selectHolesDone(); + } + func.clearPops(); + let alert = api.show.alert("analyzing parts..."); + holeSelOn = hoveredOp; + holeSelOn.classList.add("editing"); + api.feature.hover = true; + api.feature.hoverAdds = true; + func.hover = func.selectHolesHover; + func.hoverUp = func.selectHolesHoverUp; + + const widgets = kiri.api.widgets.all() + /** + * creates a mesh for a hole and adds it to a widget + * @param {Object3D} widget - widget to add the hole mesh to + * @param {Object} drill - {depth,selected} object of the hole + * @returns {Mesh} the created mesh + */ + function createHoleMesh(widget,drill){ + let {depth,selected, diam} = drill + let color = selected ? 0xFF0000:0x39e366 + let geo = new THREE.CylinderGeometry(diam/2,diam/2,depth,20); + const material = new THREE.MeshPhongMaterial( { color } ); + let mesh = new THREE.Mesh(geo, material); + mesh.position.copy(drill) + mesh.position.z -= depth/2 + mesh.rotation.x = Math.PI/2 + drill.widgetID = widget.id + drill.meshId = mesh.id; //add pointers to both objects + mesh.hole = drill; + mesh.parent = widget.mesh; + widget.mesh.add(mesh); + widget.adds.push(mesh); //for click detection + return mesh + } + let meshesCached = widgets.every(widget=>poppedRec.drills[widget.id] != undefined) + if( individual && meshesCached ){ // if any widget already has cached holes + // console.log("already has cached holes",poppedRec.drills) + api.hide.alert(alert) + kiri.api.widgets.for(widget => { + if(widget.adds){ + let drills = poppedRec.drills[widget.id] + let ids = drills + .map(hole => hole.meshId) + if(widget.adds.includes(mesh)){ + widget.adds.forEach(add=>{ + if(ids.includes(add.id)){ + add.visible = true + } + }) + }else{ + drills.forEach(drill => { + createHoleMesh(widget, drill) + }) + } + } + }) + + }else{ // if no widget has cached holes + let alert = api.show.alert(""); + await CAM.holes( + individual, + poppedRec, + (progress,msg)=>{ + alert[0]=msg + api.show.progress(progress,msg) + api.alerts.update() + }, + async centers => { + api.show.progress(0) + if(! Array.isArray(centers) ){ + console.log("worker returned a malformed drills response") + return + } + let shadow = centers.some(c=>c.shadowed) + api.hide.alert(alert) + if(shadow){ + alert = api.show.alert("Some holes are shadowed by part and are not shown."); + } + centers = centers ?? [] + // list of all hole centers and if they are selected + kiri.api.widgets.for(widget => { + const {holes} = centers.find(center=>center.id == widget.id) + // console.log(holes) + if (!holes.length) unselectHoles(holes); + holes.forEach(hole => { + createHoleMesh(widget, hole) + }) + //add hole data to record + poppedRec.drills = poppedRec.drills ?? {} + poppedRec.drills[widget.id] = holes + //give widget access to an array of drill records that refrence it + //so that it can be cleared when widget is rotated or mirrored etc. + if(!widget.drills){widget.drills = []} + widget.drills.push(holes) + }) + } + ); + } + //hide the alert once hole meshes are calculated on the worker, and then added to the scene + // api.hide.alert(alert); + let escAlert = api.show.alert("[esc] cancels drill editing",1000); + setTimeout(() => { + api.hide.alert(escAlert); + }, 5000); + kiri.api.widgets.opacity(0.8); +} + +func.selectHolesHover = function(data) { + //not used right now. may be useful in the future + if (lastTrace) { + let { color, colorSave } = lastTrace.material[0] || lastTrace.material; + color.r = colorSave.r; + color.g = colorSave.g; + color.b = colorSave.b; + lastTrace.position.z -= 0.01; + } + if (data.type === 'platform') { + lastTrace = null; + return; + } +} + +func.selectHolesHoverUp = function(int, ev) { + if (!int) return; //if not a hole mesh return + let { object } = int; + func.selectHoleToggle(object); +} + +/** + * Toggle the selection of a hole mesh and update its color + * @param {Object3D} mesh - the hole mesh to toggle + */ +func.selectHoleToggle = function(mesh) { + let {hole} = mesh + if(!hole) return + hole.selected = !hole.selected; + mesh.material.color.setHex( hole.selected ? 0xFF0000:0x39e366 ); +} + +/** + * Clears the recorded holes in the widget (widget.drills array) + * and also clears the widget.adds array. + * @param {Object} widget - the widget with the drills array to clear + */ +func.clearHolesRec = (widget)=>{ + if(widget.drills){ + widget.drills.forEach(rec=>{ + }) + } + if(widget.adds){ + widget.adds.length = 0 //clear adds array + } +} + +/** + * Cleanup function for selectHoles. + * Removes all adds from the scene, hides the alert, and resets the opacity of the widgets. + * Also resets the hover features and the editing class on the holeSelOn html element. + */ +func.selectHolesDone = () => { + if (!holeSelOn) { + return; + } + func.unpop(); + holeSelOn.classList.remove("editing"); + holeSelOn = false; + kiri.api.widgets.opacity(1); + api.hide.alert(alert); + api.feature.hover = false; + api.feature.hoverAdds = false; + + kiri.api.widgets.for(widget => { + for(let add of widget.adds){ + add.visible = false + widget.mesh.remove(add); + } + }); +}; + +func.opFlip = () => { + api.view.set_arrange(); + let widgets = api.widgets.all(); + let { process } = current; + let { ops, op2 } = process; + // add flip singleton to b-side + let add2 = op2.length === 0; + let axis = poppedRec.axis; + flipping = true; + process.camZAnchor = { + top: "bottom", + bottom: "top", + middle: "middle" + }[process.camZAnchor]; + // flip tabs + for (let widget of widgets) { + let anno = api.widgets.annotate(widget.id).tab || []; + let wbm = widget.bounds.max.z; + for (let tab of anno) { + let box = widget.tabs[tab.id].box; + let bpo = box.position; + let xr = 0, yr = 0; + let flz = wbm - bpo.z; + if (axis === 'X') { + tab.pos.y = -tab.pos.y; + bpo.y = -bpo.y; + xr = Math.PI / 2; + } + if (axis === 'Y') { + tab.pos.x = -tab.pos.x; + bpo.x = -bpo.x; + yr = Math.PI / 2; + } + tab.pos.z = bpo.z = flz; + let [ rx, ry, rz, rw ] = tab.rot; + let qat = new THREE.Quaternion(rx, ry, rz, rw); + let eul = new THREE.Euler().setFromQuaternion(qat); + eul._z = -eul._z; + tab.rot = new THREE.Quaternion().setFromEuler(eul); + } + clearTabs(widget, true); + restoreTabs([widget]); + } + // flip widget + if (axis === 'X') { + api.selection.rotate(Math.PI, 0, 0); + } + if (axis === 'Y') { + api.selection.rotate(0, Math.PI, 0); + } + // clear traces cache + CAM.traces_clear(); + api.client.clear(); + flipping = false; + process.ops = op2; + process.op2 = ops; + // flip camZBottom + if (poppedRec.invert && process.camZBottom && camZBottom) { + const maxZ = camZBottom._max.z + process.camZBottom = maxZ - process.camZBottom; + api.util.rec2ui(process); + updateStock(); + } + // keep flip operations in sync + for (let op of op2) { + if (op.type === 'flip') { + op.axis = poppedRec.axis; + op.invert = poppedRec.invert; + } + } + if (add2) { + func.opAdd(poppedRec); + } else { + func.opRender(); + } +}; + +export function init() { + + api.event.on("cam.trace.clear", func.traceClear = () => { + func.traceDone(); + api.widgets.all().forEach(widget => { + unselectTraces(widget); + }); + api.conf.save(); + }); + + api.event.on("cam.parse.gerber", opts => { + const { data, mesh } = opts; + const { open, closed, circs, rects } = load.GBR.parse(data); + const stack = new Stack(mesh || space.world.newGroup()); + const layers = new Layers(); + for (let poly of open) { + layers.setLayer("open", {line: 0xff8800}, false).addPoly(poly); + let diam = poly.tool?.shape?.diameter; + if (diam) { + const exp = poly.offset_open(diam, 'round'); + layers.setLayer("open-exp", {line: 0xff5555}, false).addPolys(exp); + } + } + for (let poly of closed) { + layers.setLayer("close", {line: 0xff0000}, false).addPoly(poly); + } + for (let poly of circs) { + layers.setLayer("circs", {line: 0x008800}, false).addPoly(poly); + } + for (let poly of rects) { + layers.setLayer("rects", {line: 0x0000ff}, false).addPoly(poly); + } + stack.addLayers(layers); + }); + + api.event.on("widget.add", widget => { + if (isCamMode && !Array.isArray(widget)) { + updateAxisMode(true); + widget.setIndexed(isIndexed ? true : false); + api.platform.update_bounds(); + } + }); + + // wire up animate button in ui + api.event.on("function.animate", (mode) => { + if (isAnimate || !isCamMode) { + return; + } + api.function.prepare(() => { + if (isCamMode) { + animate(); + } + }); + }); + + api.event.on("function.export", (mode) => { + if (isAnimate) { + isAnimate = false; + animFn().animate_clear(api); + } + }); + + api.event.on("mode.set", (mode) => { + isIndexed = undefined; + isCamMode = mode === 'CAM'; + SPACE.platform.setColor(isCamMode ? 0xeeeeee : 0xcccccc); + api.uc.setVisible(UI.func.animate, isCamMode); + // hide/show cam mode elements + for (let el of [...document.getElementsByClassName('mode-cam')]) { + api.uc.setClass(el, 'hide', !isCamMode); + } + if (!isCamMode) { + func.clearPops(); + func.tabClear(); + } + // do not persist traces across page reloads + func.traceClear(); + func.opRender(); + updateStock(); + }); + + api.event.on("view.set", (mode) => { + lastMode = mode; + isArrange = (mode === VIEWS.ARRANGE); + isPreview = (mode === VIEWS.PREVIEW); + isAnimate = (mode === VIEWS.ANIMATE); + animFn().animate_clear(api); + func.clearPops(); + if (isCamMode && isPreview) { + WIDGETS.setAxisIndex(0); + } + updateStock(); + func.opRender(); + api.uc.setVisible($('layer-animate'), isAnimate && isCamMode); + }); + + api.event.on("settings.saved", (settings) => { + validateTools(settings.tools); + current = settings; + let proc = settings.process; + let hasTabs = false; + let hasTraces = false; + if (isCamMode && proc.ops) { + proc.ops = proc.ops.filter(v => v); + } + // for any tabs or traces to set markers + for (let widget of api.widgets.all()) { + let wannot = widget.anno; + if (wannot.tab && wannot.tab.length) hasTabs = true; + if (wannot.trace && wannot.trace.length) hasTraces = true; + } + api.platform.update_bounds(); + updateIndex(); + updateStock(); + updateAxisMode(); + if (!poppedRec) { + func.opRender(); + } + }); + + api.event.on("settings.load", (settings) => { + func.opRender(); + if (!isCamMode) return; + validateTools(settings.tools); + restoreTabs(api.widgets.all()); + updateAxisMode(); + }); + + api.event.on("cam.stock.toggle", (bool) => { + camStock && (camStock.visible = bool ?? !camStock.visible); + }); + + api.event.on("boolean.click", api.platform.update_bounds); + + api.event.on([ + "init-done", + "view.set", + // update stock when modes change? + "slice.end", + "preview.end", + // update stock when preferences change + // "boolean.click", + "boolean.update", + // update stock when objects move + "platform.layout", + "selection.drag", + "selection.move", + // force bounds update, too + "selection.scale", + "selection.rotate" + ], updateStock); + + // invalidate trace and drill ops on scale or rotate + api.event.on([ + "selection.scale", + "selection.rotate" + ], () => { + if (!isCamMode) return; + for (let op of current.process.ops) { + if (op.type === 'trace' && !flipping) { + op.areas = {}; + } + else if( op.type === 'drill' && !flipping){ + op.drills = {}; + } + } + }); + + // invalidate tabs when scaleds + api.event.on([ + "selection.scale", + ], () => { + func.tabClear(); + }); + + api.event.on([ + // update tab color/opacity on dark/light change + "boolean.update" + ], updateTabs); + + api.event.on("preview.end", () => { + isParsed = false; + if (isCamMode) { + let bounds = STACKS.getStack("bounds"); + if (bounds) bounds.button("animate", animate); + } + }); + + api.event.on("code.loaded", (info) => { + if (isCamMode) { + isParsed = true; + let parse = STACKS.getStack("parse", SPACE.world); + if (parse) parse.button("animate", animate); + } + }); + + $('op-add').onmouseenter = () => { + if (func.unpop) func.unpop(); + }; + + $('op-add-list').onclick = (ev) => { + let settings = api.conf.get(); + let { process, device } = settings; + switch (ev.target.innerText.toLowerCase()) { + case "index": return func.opAddIndex(); + case "laser on": return func.opAddLaserOn(); + case "laser off": return func.opAddLaserOff(); + case "gcode": return func.opAddGCode(); + case "level": return func.opAddLevel(); + case "rough": return func.opAddRough(); + case "outline": return func.opAddOutline(); + case "contour": + let caxis = "X"; + for (let op of current.process.ops) { + if (op.type === "contour" && op.axis === "X") { + caxis = "Y"; + } + } + return func.opAddContour(caxis); + case "lathe": + let laxis = "X"; + for (let op of current.process.ops) { + if (op.type === "lathe" && op.axis === "X") { + laxis = "Y"; + } + } + return func.opAddLathe(laxis); + case "register": return func.opAddRegister('X', 2); + case "drill": return func.opAddDrill(); + case "trace": return func.opAddTrace(); + case "pocket": return func.opAddPocket(); + case "flip": + // only one flip op permitted + for (let op of current.process.ops) { + if (op.type === 'flip') { + return; + } + } + return func.opAddFlip(); + } + }; + + // TAB/TRACE BUTTON HANDLERS + api.event.on("button.click", target => { + let process = api.conf.get().process; + switch (target) { + case api.ui.tabAdd: + return func.tabAdd(); + case api.ui.tabDun: + return func.tabDone(); + case api.ui.tabClr: + api.uc.confirm("clear tabs?").then(ok => { + if (ok) func.tabClear(); + }); + break; + } + }); + + // OPS FUNCS + api.event.on("cam.op.add", func.opAdd = (rec) => { + if (!isCamMode) return; + func.clearPops(); + let oplist = current.process.ops; + if (oplist.indexOf(rec) < 0) { + if (oplist.length && oplist[oplist.length-1].type === '|') { + oplist.splice(oplist.length-1,0,rec); + } else { + oplist.push(rec); + } + let fpos = oplist.findWith(rec => rec.type === 'flip'); + if (fpos >= 0 && oplist.length > 1) { + let oprec = oplist.splice(fpos,1); + oplist.push(oprec[0]); + } + api.conf.save(); + func.opRender(); + } + }); + + api.event.on("cam.op.del", func.opDel = (rec) => { + if (!isCamMode) return; + func.clearPops(); + let oplist = current.process.ops; + let pos = oplist.indexOf(rec); + if (pos >= 0) { + oplist.splice(pos,1); + api.conf.save(); + func.opRender(); + } + }); + + // (re)render the re-orderable op list + api.event.on("cam.op.render", func.opRender = () => { + let oplist = current.process.ops; + if (!(isCamMode && oplist)) { + return; + } + oplist = oplist.filter(rec => !Array.isArray(rec)); + let mark = Date.now(); + let html = []; + let bind = {}; + let scale = api.view.unit_scale(); + let notime = false; + oplist.forEach((rec,i) => { + let title = ''; + let clock = rec.type === '|'; + let label = clock ? `` : rec.type; + let clazz = notime ? [ "draggable", "notime" ] : [ "draggable" ]; + let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined; + if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` } + if (notable?.length) label += ` (${notable[0].slice(1)})`; + html.appendAll([ + `
`, + ``, + clock ? '' : + ``, + `
` + ]); + bind[mark+i] = rec; + notime = notime || clock; + }); + let listel = $('oplist'); + listel.innerHTML = html.join(''); + let bounds = []; + let unpop = null; + let index = 0; + let indexing = true; + // drag and drop re-ordering + for (let [id, rec] of Object.entries(bind)) { + let type = rec.type; + let clock = type === '|'; + if (!clock) { + $(`${id}-x`).onmousedown = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + func.surfaceDone(); + func.traceDone(); + func.tabDone(); + func.opDel(rec); + }; + } else { + indexing = false; + } + let el = $(id); + if (!isIndexed && type === 'lathe') { + rec.disabled = true; + } + if (!hasSharedArrays && (type === 'contour' || type === 'lathe')) { + rec.disabled = true; + } + if (rec.disabled) { + el.classList.add("disabled"); + } + bounds.push(el); + let timer = null; + let inside = true; + let popped = false; + let poprec = popOp[rec.type]; + if (type === 'index' && indexing && !rec.disabled) { + index = rec.absolute ? rec.degrees : index + rec.degrees; + } + el.rec = rec; + el.unpop = () => { + let pos = [...el.childNodes].indexOf(poprec.div); + if (pos >= 0) { + el.removeChild(poprec.div); + } + popped = false; + }; + function onEnter(ev) { + if ((surfaceOn || traceOn) && poppedRec != rec) { + return; + } + if (popped && poppedRec != rec) { + func.surfaceDone(); + func.traceDone(); + } + if (unpop) unpop(); + unpop = func.unpop = el.unpop; + inside = true; + // pointer to current rec for trace editing + poppedRec = rec; + popped = true; + poprec.use(rec); + hoveredOp = el; + if (!clock) { + // offset Y position of pop div by % of Y screen location of button + el.appendChild(poprec.div); + poprec.addNote(); + const { innerHeight } = window; + const brect = ev.target.getBoundingClientRect(); + const prect = poprec.div.getBoundingClientRect(); + const pcty = (brect.top / innerHeight) * 0.9; + const offpx = -pcty * prect.height; + poprec.div.style.transform = `translateY(${offpx}px)`; + } + // option click event appears latent + // and overides the sticky settings + setTimeout(() => { + UC.setSticky(false); + }, 0); + } + function onLeave(ev) { + inside = false; + clearTimeout(timer); + timer = setTimeout(() => { + if (!inside && poprec.using(rec) && !UC.isSticky()) { + el.unpop(); + } + }, 250); + } + function onDown(ev) { + if (!ev.target.rec) { + // only trigger on operation buttons bound to recs + return; + } + let mobile = ev.touches; + func.surfaceDone(); + func.traceDone(); + let target = ev.target, clist = target.classList; + if (!clist.contains("draggable")) { + return; + } + // toggle enable / disable + if (!clock && (ev.ctrlKey || ev.metaKey)) { + ev.preventDefault(); + ev.stopPropagation(); + rec.disabled = !rec.disabled; + for (let op of ev.shiftKey ? oplist : [ rec ]) { + if (op !== rec) { + op.disabled = op.type !== '|' ? !rec.disabled : false; + } + } + for (let el of bounds) { + if (el.rec.disabled) { + el.classList.add("disabled"); + } else { + el.classList.remove("disabled"); + } + } + if (isIndexed) { + updateIndex(); + } + return true; + } + // duplicate op + if (!clock && ev.shiftKey) { + ev.preventDefault(); + ev.stopPropagation(); + oplist = current.process.ops; + oplist.push(Object.clone(rec)); + api.conf.save(); + func.opRender(); + return true; + } + clist.add("drag"); + ev.stopPropagation(); + ev.preventDefault(); + let tracker = UI.tracker; + tracker.style.display = 'block'; + let cancel = tracker.onmouseup = (ev) => { + oplist = current.process.ops; + clist.remove("drag"); + tracker.style.display = 'none'; + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + oplist.length = 0; + for (let child of listel.childNodes) { + oplist.push(child.rec); + } + api.conf.save(); + func.opRender(); + if (mobile) { + el.ontouchmove = onDown; + el.ontouchend = undefined; + } + }; + function onMove(ev) { + ev.stopPropagation(); + ev.preventDefault(); + if (ev.buttons === 0) { + return cancel(); + } + for (let el of bounds) { + if (el === target) continue; + let rect = el.getBoundingClientRect(); + let top = rect.top; + let bottom = rect.bottom;// + rect.height; + let tar = mobile ? ev.touches[0] : ev; + if (tar.pageY >= top && tar.pageY <= bottom) { + let mid = (top + bottom) / 2; + try { listel.removeChild(target); } catch (e) { } + el.insertAdjacentElement(tar.pageY < mid ? "beforebegin" : "afterend", target); + } + } + } + tracker.onmousemove = onMove; + if (mobile) { + el.ontouchmove = onMove; + el.ontouchend = cancel; + } + } + if (SPACE.info.mob) { + let touched = false; + el.ontouchstart = (ev) => { + touched = true; + if (poppedRec === rec && popped) { + onLeave(ev); + } else { + onEnter(ev); + } + }; + el.ontouchmove = onDown; + el.onmouseenter = (ev) => { + if (touched) { + // touches block mouse events on touchscreen PCs + // which are often sent along with touch events + // but when not touched, allow the mouse to work + return; + } + el.onmousedown = onDown; + el.onmouseleave = onLeave; + onEnter(ev); + }; + } else { + el.onmousedown = onDown; + el.onmouseenter = onEnter; + el.onmouseleave = onLeave; + } + } + if (lastMode !== VIEWS.ANIMATE) { + // update widget rotations from timeline marker + WIDGETS.setAxisIndex(isPreview || !isIndexed ? 0 : -index); + } + currentIndex = isIndexed && !isPreview ? index * DEG2RAD : 0; + updateStock(); + }); + + // TAB FUNCS + let showTab, lastTab, tab, iw, ic; + api.event.on("cam.tabs.add", func.tabAdd = () => { + func.traceDone(); + alert = api.show.alert("[esc] cancels tab editing"); + api.feature.hover = true; + func.hover = func.tabHover; + func.hoverUp = func.tabHoverUp; + }); + + api.event.on("cam.tabs.done", func.tabDone = () => { + delbox('tabb'); + api.hide.alert(alert); + api.feature.hover = false; + if (lastTab) { + lastTab.box.material.color.r = 0; + lastTab = null; + } + }); + + api.event.on("cam.tabs.clear", func.tabClear = () => { + func.tabDone(); + api.widgets.all().forEach(widget => { + clearTabs(widget); + widget.saveState(); + }); + api.conf.save(); + }); + + // COMMON TAB/TRACE EVENT HANDLERS + api.event.on("slice.begin", () => { + if (isCamMode) { + func.clearPops(); + } + }); + + api.event.on("key.esc", () => { + if (isCamMode) { + func.clearPops(); + } + }); + + api.event.on("selection.scale", () => { + if (isCamMode) { + func.clearPops(); + } + }); + + api.event.on("widget.duplicate", (widget, oldwidget) => { + if (!isCamMode) { + return; + } + if (traceOn) { + func.traceDone(); + } + unselectTraces(widget); + if (flipping) { + return; + } + let ann = api.widgets.annotate(widget.id); + if (ann.tab) { + ann.tab.forEach((tab,i) => { + tab.id = Date.now() + i; + }); + restoreTabs([widget]); + } + }); + + api.event.on("widget.mirror", widget => { + if (!isCamMode) { + return; + } + if (traceOn) { + func.traceDone(); + } + if (holeSelOn) { + func.selectHolesDone(); + } + func.clearHolesRec(widget) + unselectTraces(widget); + if (flipping) { + return; + } + mirrorTabs(widget); + }); + + api.event.on("widget.rotate", rot => { + if (!isCamMode) { + return; + } + let {widget, x, y, z} = rot; + if (traceOn) { + func.traceDone(); + } + unselectTraces(widget); + if (holeSelOn) { + func.selectHolesDone(); + } + if (flipping) { + return; + } + func.clearHolesRec(widget) + if (x || y) { + clearTabs(widget); + } else { + rotateTabs(widget, x, y, z); + } + }); + + api.event.on("mouse.hover.up", rec => { + if (!isCamMode) { + return; + } + let { object, event } = rec; + func.hoverUp(object, event); + }); + + api.event.on("mouse.hover", data => { + if (!isCamMode) { + return; + } + func.hover(data); + }); + + createPopOp('level', { + tool: 'camLevelTool', + spindle: 'camLevelSpindle', + step: 'camLevelOver', + rate: 'camLevelSpeed', + down: 'camLevelDown', + over: 'camLevelOver', + stock: 'camLevelStock' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + down: UC.newInput(LANG.cc_loff_s, {title:LANG.cc_loff_l, convert:UC.toFloat, units:true}), + over: UC.newInput(LANG.cc_lxyo_s, {title:LANG.cc_lxyo_l, convert:UC.toFloat, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + stock: UC.newBoolean(LANG.cc_lsto_s, undefined, {title:LANG.cc_lsto_l}), + }; + + createPopOp('rough', { + tool: 'camRoughTool', + spindle: 'camRoughSpindle', + down: 'camRoughDown', + step: 'camRoughOver', + rate: 'camRoughSpeed', + plunge: 'camRoughPlunge', + leave: 'camRoughStock', + leavez: 'camRoughStockZ', + all: 'camRoughAll', + voids: 'camRoughVoid', + flats: 'camRoughFlat', + inside: 'camRoughIn', + ov_topz: 0, + ov_botz: 0, + ov_conv: '~camConventional', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), + leave: UC.newInput(LANG.cr_lsto_s, {title:LANG.cr_lsto_l, convert:UC.toFloat, units:true}), + leavez: UC.newInput(LANG.cr_lstz_s, {title:LANG.cr_lstz_l, convert:UC.toFloat, bound:UC.bound(0,10),units:true}), + sep: UC.newBlank({class:"pop-sep"}), + all: UC.newBoolean(LANG.cr_clst_s, undefined, {title:LANG.cr_clst_l, show:hasIndexing}), + voids: UC.newBoolean(LANG.cr_clrp_s, undefined, {title:LANG.cr_clrp_l}), + flats: UC.newBoolean(LANG.cr_clrf_s, undefined, {title:LANG.cr_clrf_l}), + inside: UC.newBoolean(LANG.cr_olin_s, undefined, {title:LANG.cr_olin_l}), + sep: UC.newBlank({class:"pop-sep"}), + exp: UC.newExpand("overrides"), + ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), + ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), + ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), + exp_end: UC.endExpand(), + }; + + createPopOp('outline', { + tool: 'camOutlineTool', + spindle: 'camOutlineSpindle', + step: 'camOutlineOver', + steps: 'camOutlineOverCount', + down: 'camOutlineDown', + rate: 'camOutlineSpeed', + plunge: 'camOutlinePlunge', + dogbones: 'camOutlineDogbone', + omitvoid: 'camOutlineOmitVoid', + omitthru: 'camOutlineOmitThru', + outside: 'camOutlineOut', + inside: 'camOutlineIn', + wide: 'camOutlineWide', + top: 'camOutlineTop', + ov_topz: 0, + ov_botz: 0, + ov_conv: '~camConventional', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0), show:() => popOp.outline.rec.wide}), + steps: UC.newInput(LANG.cc_sovc_s, {title:LANG.cc_sovc_l, convert:UC.toInt, bound:UC.bound(1,5), show:() => popOp.outline.rec.wide}), + sep: UC.newBlank({class:"pop-sep"}), + top: UC.newBoolean(LANG.co_clrt_s, undefined, {title:LANG.co_clrt_l}), + inside: UC.newBoolean(LANG.co_olin_s, undefined, {title:LANG.co_olin_l, show:(op) => { return !op.inputs.outside.checked }}), + outside: UC.newBoolean(LANG.co_olot_s, undefined, {title:LANG.co_olot_l, show:(op) => { return !op.inputs.inside.checked }}), + sep: UC.newBlank({class:"pop-sep"}), + omitthru: UC.newBoolean(LANG.co_omit_s, undefined, {title:LANG.co_omit_l, xshow:(op) => { return op.inputs.outside.checked }}), + omitvoid: UC.newBoolean(LANG.co_omvd_s, undefined, {title:LANG.co_omvd_l, xshow:(op) => { return op.inputs.outside.checked }}), + wide: UC.newBoolean(LANG.co_wide_s, undefined, {title:LANG.co_wide_l, show:(op) => { return !op.inputs.inside.checked }}), + dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, {title:LANG.co_dogb_l, show:(op) => { return !op.inputs.wide.checked }}), + sep: UC.newBlank({class:"pop-sep"}), + exp: UC.newExpand("overrides"), + ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), + ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), + ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), + exp_end: UC.endExpand(), + }; + + const contourFilter = gcodeEditor('Layer Filter', 'filter'); + + createPopOp('contour', { + tool: 'camContourTool', + spindle: 'camContourSpindle', + step: 'camContourOver', + rate: 'camContourSpeed', + angle: 'camContourAngle', + leave: 'camContourLeave', + tolerance: 'camTolerance', + flatness: 'camFlatness', + reduction: 'camContourReduce', + bridging: 'camContourBridge', + bottom: 'camContourBottom', + curves: 'camContourCurves', + inside: 'camContourIn', + filter: 'camContourFilter', + axis: 'X' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,10.0)}), + leave: UC.newInput(LANG.cf_leav_s, {title:LANG.cf_leav_l, convert:UC.toFloat, bound:UC.bound(0,100)}), + sep: UC.newBlank({class:"pop-sep"}), + angle: UC.newInput(LANG.cf_angl_s, {title:LANG.cf_angl_l, convert:UC.toFloat, bound:UC.bound(45,90), show:(op) => op.inputs.curves.checked}), + flatness: UC.newInput(LANG.ou_flat_s, {title:LANG.ou_flat_l, convert:UC.toFloat, bound:UC.bound(0,1.0), units:false, round:4}), + tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true, round:4}), + reduction: UC.newInput(LANG.ou_redu_s, {title:LANG.ou_redu_l, convert:UC.toInt, bound:UC.bound(0,10), units:false}), + // bridging: UC.newInput(LANG.ou_brdg_s, {title:LANG.ou_brdg_l, convert:UC.toFloat, bound:UC.bound(0,1000.0), units:true, round:4, show:(op) => op.inputs.curves.checked}), + sep: UC.newBlank({class:"pop-sep"}), + curves: UC.newBoolean(LANG.cf_curv_s, undefined, {title:LANG.cf_curv_l}), + inside: UC.newBoolean(LANG.cf_olin_s, undefined, {title:LANG.cf_olin_l}), + bottom: UC.newBoolean(LANG.cf_botm_s, undefined, {title:LANG.cf_botm_l, show:(op,conf) => conf ? conf.process.camZBottom : 0}), + filter: UC.newRow([ UC.newButton(LANG.filter, contourFilter) ], {class:"ext-buttons f-row"}) + }; + + createPopOp('lathe', { + tool: 'camLatheTool', + spindle: 'camLatheSpindle', + step: 'camLatheOver', + angle: 'camLatheAngle', + rate: 'camLatheSpeed', + tolerance: 'camTolerance', + filter: 'camContourFilter', + leave: 'camContourLeave', + linear: 'camLatheLinear' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + // axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,100.0)}), + angle: UC.newInput(LANG.cc_sang_s, {title:LANG.cc_sang_l, convert:UC.toFloat, bound:UC.bound(0.01,180.0)}), + sep: UC.newBlank({class:"pop-sep"}), + tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true, round:4}), + leave: UC.newInput(LANG.cf_leav_s, {title:LANG.cf_leav_l, convert:UC.toFloat, bound:UC.bound(0,100)}), + sep: UC.newBlank({class:"pop-sep"}), + linear: UC.newBoolean(LANG.ci_line_s, undefined, {title:LANG.ci_line_l}), + // filter: UC.newRow([ UC.newButton(LANG.filter, contourFilter) ], {class:"ext-buttons f-row"}) + }; + + function canDogBones() { + if (!poppedRec) return false; + return poppedRec.mode === 'follow';// && poppedRec.offset && poppedRec.offset !== 'none'; + } + + function canDogBonesRev() { + return canDogBones() && poppedRec.dogbone; + } + + function zDogSep() { + return canDogBones() || zBottom(); + } + + createPopOp('trace', { + mode: 'camTraceType', + offset: 'camTraceOffset', + spindle: 'camTraceSpindle', + tool: 'camTraceTool', + step: 'camTraceOver', + down: 'camTraceDown', + thru: 'camTraceThru', + rate: 'camTraceSpeed', + plunge: 'camTracePlunge', + offover: 'camTraceOffOver', + dogbone: 'camTraceDogbone', + revbone: 'camTraceDogbone', + merge: 'camTraceMerge', + ov_topz: 0, + ov_botz: 0, + ov_conv: '~camConventional', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + mode: UC.newSelect(LANG.cu_type_s, {title:LANG.cu_type_l}, "trace"), + offset: UC.newSelect(LANG.cc_offs_s, {title: LANG.cc_offs_l, show:() => (poppedRec.mode === 'follow')}, "traceoff"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0), show:(op) => popOp.trace.rec.mode === "clear"}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + thru: UC.newInput(LANG.cc_thru_s, {title:LANG.cc_thru_l, convert:UC.toFloat, units:true}), + offover: UC.newInput(LANG.cc_offd_s, {title:LANG.cc_offd_l, convert:UC.toFloat, units:true, show:() => poppedRec.offset !== "none" || poppedRec.mode === "clear"}), + sep: UC.newBlank({class:"pop-sep", modes:MCAM, xshow:zDogSep}), + merge: UC.newBoolean(LANG.co_merg_s, undefined, {title:LANG.co_merg_l, show:() => !popOp.trace.rec.down}), + dogbone: UC.newBoolean(LANG.co_dogb_s, undefined, {title:LANG.co_dogb_l, show:canDogBones}), + revbone: UC.newBoolean(LANG.co_dogr_s, undefined, {title:LANG.co_dogr_l, show:canDogBonesRev}), + exp: UC.newExpand("overrides"), + sep: UC.newBlank({class:"pop-sep"}), + ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), + ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), + ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), + exp_end: UC.endExpand(), + sep: UC.newBlank({class:"pop-sep"}), + menu: UC.newRow([ UC.newButton("select", func.traceAdd) ], {class:"ext-buttons f-row"}), + }; + + createPopOp('pocket', { + spindle: 'camPocketSpindle', + tool: 'camPocketTool', + step: 'camPocketOver', + down: 'camPocketDown', + rate: 'camPocketSpeed', + plunge: 'camPocketPlunge', + expand: 'camPocketExpand', + smooth: 'camPocketSmooth', + refine: 'camPocketRefine', + follow: 'camPocketFollow', + contour: 'camPocketContour', + engrave: 'camPocketEngrave', + outline: 'camPocketOutline', + ov_topz: 0, + ov_botz: 0, + ov_conv: '~camConventional', + tolerance: 'camTolerance', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true, show:() => !poppedRec.contour}), + sep: UC.newBlank({class:"pop-sep"}), + expand: UC.newInput(LANG.cp_xpnd_s, {title:LANG.cp_xpnd_l, convert:UC.toFloat, units:true, xshow:() => !poppedRec.contour}), + refine: UC.newInput(LANG.cp_refi_s, {title:LANG.cp_refi_l, convert:UC.toInt, show:() => poppedRec.contour}), + smooth: UC.newInput(LANG.cp_smoo_s, {title:LANG.cp_smoo_l, convert:UC.toInt}), + tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true, round:4, show:() => poppedRec.contour}), + follow: UC.newInput(LANG.cp_foll_s, {title:LANG.cp_foll_l, convert:UC.toFloat}), + sep: UC.newBlank({class:"pop-sep"}), + contour: UC.newBoolean(LANG.cp_cont_s, undefined, {title:LANG.cp_cont_s}), + engrave: UC.newBoolean(LANG.cp_engr_s, undefined, {title:LANG.cp_engr_l, show:() => poppedRec.contour}), + outline: UC.newBoolean(LANG.cp_outl_s, undefined, {title:LANG.cp_outl_l}), + exp: UC.newExpand("overrides"), + sep: UC.newBlank({class:"pop-sep"}), + ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), + ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), + ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), + exp_end: UC.endExpand(), + sep: UC.newBlank({class:"pop-sep"}), + menu: UC.newRow([ UC.newButton("select", func.surfaceAdd) ], {class:"ext-buttons f-row"}), + }; + + createPopOp('drill', { + tool: 'camDrillTool', + spindle: 'camDrillSpindle', + down: 'camDrillDown', + rate: 'camDrillDownSpeed', + dwell: 'camDrillDwell', + lift: 'camDrillLift', + mark: 'camDrillMark', + precision:'camDrillPrecision', + thru: 'camDrillThru', + fromTop: 'camDrillFromStockTop', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + dwell: UC.newInput(LANG.cd_dwll_s, {title:LANG.cd_dwll_l, convert:UC.toFloat}), + lift: UC.newInput(LANG.cd_lift_s, {title:LANG.cd_lift_l, convert:UC.toFloat, units:true, show:() => !poppedRec.mark}), + mark: UC.newBoolean(LANG.cd_mark_s,undefined, {title:LANG.cd_mark_l, show:() => !poppedRec.fromTop}), + fromTop: UC.newBoolean(LANG.cd_ftop_s,undefined, {title:LANG.cd_ftop_l, show:() => !poppedRec.mark}), + sep: UC.newBlank({class:"pop-sep"}), + thru: UC.newInput(LANG.cd_dtru_s, {title:LANG.cd_dtru_l, convert:UC.toFloat, units:true,show:() => !poppedRec.mark}), + precision:UC.newInput(LANG.cd_prcn_s, {title:LANG.cd_prcn_l, convert:UC.toFloat, units:true,show:() => !poppedRec.mark}), + sep: UC.newBlank({class:"pop-sep", }), + actions: UC.newRow([ + UC.newButton(LANG.select, ()=>func.selectHoles(true), {title:LANG.cd_seli_l}), + UC.newButton(LANG.cd_sela_s, ()=>func.selectHoles(false), {title:LANG.cd_sela_l}) + ], {class:"ext-buttons f-col"}) + }; + + createPopOp('register', { + tool: 'camDrillTool', + spindle: 'camDrillSpindle', + down: 'camDrillDown', + rate: 'camDrillDownSpeed', + dwell: 'camDrillDwell', + lift: 'camDrillLift', + feed: 'camRegisterSpeed', + thru: 'camRegisterThru' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), + points: UC.newSelect(LANG.cd_points, {show:() => poppedRec.axis !== '-'}, "regpoints"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + feed: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true, show:() => poppedRec.axis === '-'}), + sep: UC.newBlank({class:"pop-sep"}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + dwell: UC.newInput(LANG.cd_dwll_s, {title:LANG.cd_dwll_l, convert:UC.toFloat, show:() => poppedRec.axis !== '-'}), + lift: UC.newInput(LANG.cd_lift_s, {title:LANG.cd_lift_l, convert:UC.toFloat, units:true, show:() => poppedRec.axis !== '-'}), + sep: UC.newBlank({class:"pop-sep"}), + thru: UC.newInput(LANG.cd_thru_s, {title:LANG.cd_thru_l, convert:UC.toFloat, units:true}), + }; + + createPopOp('flip', { + axis: 'camFlipAxis', + invert: 'camFlipInvert' + }).inputs = { + axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), + sep: UC.newBlank({class:"pop-sep", modes:MCAM, show:zBottom}), + invert: UC.newBoolean(LANG.cf_nvrt_s, undefined, {title:LANG.cf_nvrt_l, show:zBottom}), + sep: UC.newBlank({class:"pop-sep"}), + action: UC.newRow([ UC.newButton(LANG.cf_menu, func.opFlip) ], {class:"ext-buttons f-row"}) + }; + + createPopOp('gcode', { + gcode: 'camCustomGcode', + }).inputs = { + action: UC.newRow([ UC.newButton(LANG.edit, gcodeEditor()) ], {class:"ext-buttons f-row"}) + }; + + function angleTowardZUp() { + api.event.emit('tool.mesh.face-up'); + } + + api.event.on('tool.mesh.face-normal', normal => { + // console.log({ poppedRec }); + poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2); + poppedRec.absolute = true; + func.opRender(); + updateStock(); + }); + + createPopOp('index', { + degrees: 'camIndexAxis', + absolute: 'camIndexAbs' + }).inputs = { + degrees: UC.newInput(LANG.ci_degr_s, {title:LANG.ci_degr_l, convert:UC.toFloat, bound:UC.bound(-360,360.0) }), + absolute: UC.newBoolean(LANG.ci_abso_s, undefined, {title:LANG.ci_abso_l}), + select: UC.newRow([ UC.newButton(LANG.ci_face_s, angleTowardZUp) ], {class:"ext-buttons f-row"}) + }; + + const editEnable = gcodeEditor('Laser Enable Script', 'enable'); + const editOn = gcodeEditor('Laser On Script', 'on'); + const editOff = gcodeEditor('Laser Off Script', 'off'); + + createPopOp('laser on', { + enable: 'camLaserEnable', + on: 'camLaserOn', + off: 'camLaserOff', + power: 'camLaserPower', + adapt: 'camLaserAdaptive', + adaptrp: 'camLaserAdaptMod', + flat: 'camLaserFlatten', + flatz: 'camLaserFlatZ', + minp: 'camLaserPowerMin', + maxp: 'camLaserPowerMax', + minz: 'camLaserZMin', + maxz: 'camLaserZMax', + }).inputs = { + enable: UC.newRow([ UC.newButton(LANG.enable, editEnable) ], {class:"ext-buttons f-row"}), + on: UC.newRow([ UC.newButton(LANG.on, editOn) ], {class:"ext-buttons f-row"}), + off: UC.newRow([ UC.newButton(LANG.off, editOff) ], {class:"ext-buttons f-row"}), + sep: UC.newBlank({class:"pop-sep"}), + power: UC.newInput(LANG.cl_powr_s, {title:LANG.cl_powr_l, convert:UC.toFloat, bound:UC.bound(0,1.0), show:() => !poppedRec.adapt}), + maxp: UC.newInput(LANG.cl_maxp_s, {title:LANG.cl_maxp_l, convert:UC.toFloat, bound:UC.bound(0,1.0), show:() => poppedRec.adapt}), + minp: UC.newInput(LANG.cl_minp_s, {title:LANG.cl_minp_l, convert:UC.toFloat, bound:UC.bound(0,1.0), show:() => poppedRec.adapt}), + maxz: UC.newInput(LANG.cl_maxz_s, {title:LANG.cl_maxz_l, convert:UC.toFloat, show:() => poppedRec.adapt}), + minz: UC.newInput(LANG.cl_minz_s, {title:LANG.cl_minz_l, convert:UC.toFloat, show:() => poppedRec.adapt}), + flatz: UC.newInput(LANG.cl_flaz_s, {title:LANG.cl_flaz_l, convert:UC.toFloat, show:() => poppedRec.flat}), + sep: UC.newBlank({class:"pop-sep"}), + adapt: UC.newBoolean(LANG.cl_adap_s, undefined, {title:LANG.cl_adap_l}), + adaptrp: UC.newBoolean(LANG.cl_adrp_s, undefined, {title:LANG.cl_adrp_l, show:() => poppedRec.adapt}), + flat: UC.newBoolean(LANG.cl_flat_s, undefined, {title:LANG.cl_flat_l }), + }; + + const editDisable = gcodeEditor('Laser Disable Script', 'disable'); + + createPopOp('laser off', { + disable: 'camLaserDisable', + }).inputs = { + disable: UC.newRow([ UC.newButton(LANG.disable, editDisable) ], {class:"ext-buttons f-row"}), + }; + + createPopOp('|', {}).inputs = {}; +}; + +/** + * Create a new popup operation (popOp) for a given type. + * @param {string} type - the name of the popOp to create + * @param {object} map - an object mapping popOp keys to either a string + * (representing a key in current.process) or a value (to be used as the default). + * @returns {object} the newly created popOp. + */ +function createPopOp(type, map) { + let op = popOp[type] = { + div: UC.newElement('div', { id:`${type}-op`, class:"cam-pop-op" }), + use: (rec) => { + op.rec = rec; + for (let [key, val] of Object.entries(op.inputs)) { + let type = val.type; + let from = map[key]; + let rval = rec[key]; + // fill undef entries older defines + if (type && (rval === null || rval === undefined)) { + if (typeof(from) === 'string') { + rec[key] = current.process[from]; + } else if (from !== undefined) { + rec[key] = from; + } else { + console.log('error', { key, val, type, from }); + } + } + } + api.util.rec2ui(rec, op.inputs); + op.hideshow(); + }, + using: (rec) => { + return op.rec === rec; + }, + bind: (ev) => { + api.util.ui2rec(op.rec, op.inputs); + + const settings = conf.get(); + const {tool} = new CAM.Tool(settings,op.rec.tool); //get tool by id + const opType = op.rec.type + const drillOrRegister = opType == "drill" || opType == "register" + + if ( !drillOrRegister && tool.type == "drill"){ + alerts.show(`Warning: Drills should not be used for ${opType} operations.`) + } + else if ( drillOrRegister && tool.type != "drill"){ + alerts.show(`Warning: Only drills should be used for drilling operations.`) + } + + for (let [key, val] of Object.entries(op.rec)) { + let saveTo = map[key]; + if (saveTo && typeof(key) === 'string' && !key.startsWith("~")) { + current.process[saveTo] = val; + } + } + api.conf.save(); + op.hideshow(); + }, + new: () => { + let rec = { type }; + for (let [key, src] of Object.entries(map)) { + rec[key] = typeof(src) === 'string' + ? current.process[src.replace('~','')] + : src; + } + return rec; + }, + hideshow: () => { + for (let inp of Object.values(op.inputs)) { + let parent = inp.parentElement; + if (parent && parent.setVisible && parent.__opt.show) { + parent.setVisible(parent.__opt.show(op, api.conf.get())); + } + } + }, + addNote: () => { + if (!op.note && type !== 'flip') { + const divid = `div-${++seed}`; + const noteid = `note-${++seed}`; + const div = document.createElement('div'); + div.setAttribute('id', divid); + div.classList.add('pop-tics') + op.div.appendChild(div); + div.innerHTML = h.build( + h.div([ h.label({ id: noteid }) ]) + ); + op.note = { divid, noteid }; + } + if (op.note) { + const { divid, noteid } = op.note; + const div = $(divid); + if (div) div.onclick = () => { + api.uc.prompt('Edit Note for Operation', poppedRec.note || '').then(note => { + if (note !== undefined && note !== null) { + poppedRec.note = op.x = note; + api.conf.save(); + } + func.opRender(); + }); + }; + const note = $(noteid); + if (note) note.innerText = poppedRec.note || ''; + } + }, + group: [], + }; + + /** + * @function createRecordGetter + * @description Creates a getter function for the current record. + * The getter will bind the current record to the op before returning it. + * This is useful for passing a record to a pre-slice function + * @returns {function} A getter function for the current record. + */ + op.createRecordGetter = () => { + return () => { + op.bind(); + return op.rec; + } + } + + UC.restore({ + addTo: op.div, + bindTo: op.bind, + lastDiv: op.div, + lastGroup: op.group + }); + return op; +} + +function createTabBox(iw, ic, n) { + const { track } = iw; + const { stock, bounds, process } = api.conf.get(); + const { camTabsWidth, camTabsHeight, camTabsDepth, camTabsMidline } = process; + const { camZBottom, camStockIndexed } = process; + const isIndexed = camStockIndexed; + const sz = stock.z || bounds.max.z; + const zto = sz - iw.track.top; + const zp = (camZBottom || isIndexed ? camZBottom : sz - track.box.d - zto) + (camTabsMidline ? 0 : camTabsHeight / 2); + ic.x += n.x * camTabsDepth / 2; // offset from part + ic.z -= n.y * camTabsDepth / 2; // offset swap z,y + ic.y = zp; // offset swap in world space y,z + const rot = new THREE.Quaternion().setFromAxisAngle(zaxis, Math.atan2(n.y, n.x)); + const pos = { x:ic.x, y:ic.y, z:ic.z }; + const dim = { x:camTabsDepth, y:camTabsWidth, z:camTabsHeight }; + const tab = addbox(pos, boxColor(), 'tabb', dim, { rotate: rot, opacity: boxOpacity() }); + return { pos, dim, rot, tab, width: camTabsWidth, height: camTabsHeight, stock }; +} + +function addWidgetTab(widget, rec) { + const { pos, dim, rot, id } = rec; + const tabs = widget.tabs = (widget.tabs || {}); + // prevent duplicate restore from repeated settings load calls + if (!tabs[id]) { + pos.box = addbox( + pos, boxColor(), id, + dim, { group: widget.mesh, rotate: rot, opacity: boxOpacity() } + ); + pos.box.tab = Object.assign({widget, id}, pos); + widget.adds.push(pos.box); + tabs[id] = pos; + } +} + +export function restoreTabs(widgets) { + widgets.forEach(widget => { + const tabs = api.widgets.annotate(widget.id).tab || []; + tabs.forEach(rec => { + let [ x, y, z, w ] = rec.rot; + rec = Object.clone(rec); + rec.rot = new THREE.Quaternion(x, y, z, w); + addWidgetTab(widget, rec); + }); + }); +} + +function clearTabs(widget, skiprec) { + Object.values(widget.tabs || {}).forEach(rec => { + widget.adds.remove(rec.box); + widget.mesh.remove(rec.box); + }); + widget.tabs = {}; + if (!skiprec) { + delete api.widgets.annotate(widget.id).tab; + } +} + +function updateTabs() { + // update tab color and opacity + api.widgets.all().forEach(widget => { + Object.values(widget.tabs || {}).forEach(rec => { + for (let rec of widget.adds || []) { + rec.material.color = new THREE.Color(boxColor()); + rec.material.opacity = boxOpacity(); + } + }); + }); +} + +function unselectTraces(widget, skip) { + if (widget.trace_stack) { + widget.trace_stack.meshes.forEach(mesh => { + if (mesh.selected) { + func.traceToggle(mesh, skip); + } + }); + } +} + +function unselectHoles(widget) { + if (!widget.holes) return + widget.holes.forEach(hole => { + hole.selected = false + }) +} + +function validateTools(tools) { + if (tools) { + let max = 0; + for (let t of tools) { + if (Number.isInteger(t.number)) { + max = Math.max(max, t.number); + } + } + for (let t of tools) { + if (!Number.isInteger(t.number)) { + t.number = ++max; + console.log('added tool #', t); + } + } + } +} + +function isDark() { return api.space.is_dark() }; + +function boxColor() { + return isDark() ? 0x00ddff : 0x0000dd; +} + +function boxOpacity() { + return isDark() ? 0.75 : 0.6; +} + +function animate() { + isAnimate = true; + api.widgets.opacity(isParsed ? 0 : 0.75); + api.hide.slider(); + STACKS.clear(); + animFn().animate(api); + api.view.set_animate(); +} + +function updateStock() { + if (isAnimate) { + if (isIndexed) { + SPACE.world.remove(camStock); + camStock = undefined; + } + return; + } + + if (!isCamMode) { + SPACE.world.remove(camZTop); + SPACE.world.remove(camZBottom); + SPACE.world.remove(camStock); + camStock = null; + camZTop = null; + camZBottom = null; + return; + } + + api.platform.update_bounds(); + + const settings = api.conf.get(); + const widgets = api.widgets.all(); + + const { stock, process } = settings; + const { x, y, z, center } = stock; + + UI.func.animate.classList.add('disabled'); + if (camStock) { + SPACE.world.remove(camStock); + camStock = null; + } + if (x && y && z) { + UI.func.animate.classList.remove('disabled'); + { + let geo = new THREE.BoxGeometry(1, 1, 1); + let mat = new THREE.MeshBasicMaterial({ + color: 0x777777, + opacity: 0.05, + transparent: true, + side:THREE.DoubleSide + }); + camStock = new THREE.Mesh(geo, mat); + camStock.renderOrder = 2; + + let lo = 0.5; + let lidat = [ + lo, lo, lo, lo, lo,-lo, + lo, lo, lo, lo,-lo, lo, + lo, lo, lo, -lo, lo, lo, + -lo,-lo,-lo, -lo,-lo, lo, + -lo,-lo,-lo, -lo, lo,-lo, + -lo,-lo,-lo, lo,-lo,-lo, + lo, lo,-lo, -lo, lo,-lo, + lo, lo,-lo, lo,-lo,-lo, + lo,-lo,-lo, lo,-lo, lo, + lo,-lo, lo, -lo,-lo, lo, + -lo,-lo, lo, -lo, lo, lo, + -lo, lo, lo, -lo, lo,-lo + ]; + let ligeo = new THREE.BufferGeometry(); + ligeo.setAttribute('position', new THREE.BufferAttribute(lidat.toFloat32(), 3)); + let limat = new THREE.LineBasicMaterial({ color: 0xaaaaaa }); + let lines = new THREE.LineSegments(ligeo, limat); + camStock.lines = lines; + camStock.add(lines); + SPACE.world.add(camStock); + } + // fight z fighting in threejs + camStock.scale.x = x + 0.005; + camStock.scale.y = y + 0.005; + camStock.scale.z = z + 0.005; + camStock.position.x = center.x; + camStock.position.y = center.y; + camStock.position.z = center.z; + camStock.lines.material.color = + new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa); + } + + SPACE.world.remove(camZTop); + if (process.camZTop && widgets.length) { + let max = { x, y, z }; + for (let w of widgets) { + max.x = Math.max(max.x, w.track.box.w); + max.y = Math.max(max.y, w.track.box.h); + max.z = Math.max(max.z, w.track.box.d); + } + let geo = new THREE.PlaneGeometry(max.x, max.y); + let mat = new THREE.MeshBasicMaterial({ + color: 0x777777, + opacity: 0.55, + transparent: true, + side:THREE.DoubleSide + }); + camZTop = new THREE.Mesh(geo, mat); + camZTop._max = max; + camZTop.renderOrder = 1; + camZTop.position.x = center.x; + camZTop.position.y = center.y; + camZTop.position.z = process.camZTop; + SPACE.world.add(camZTop); + } else { + camZTop = undefined; + } + + SPACE.world.remove(camZBottom); + if (process.camZBottom && widgets.length) { + let max = { x, y, z }; + for (let w of widgets) { + max.x = Math.max(max.x, w.track.box.w); + max.y = Math.max(max.y, w.track.box.h); + max.z = Math.max(max.z, w.track.box.d); + } + let geo = new THREE.PlaneGeometry(max.x, max.y); + let mat = new THREE.MeshBasicMaterial({ + color: 0x777777, + opacity: 0.55, + transparent: true, + side:THREE.DoubleSide + }); + camZBottom = new THREE.Mesh(geo, mat); + camZBottom._max = max; + camZBottom.renderOrder = 1; + camZBottom.position.x = center.x; + camZBottom.position.y = center.y; + camZBottom.position.z = process.camZBottom; + SPACE.world.add(camZBottom); + } else { + camZBottom = undefined; + } + + SPACE.update(); +} diff --git a/src2/kiri-mode/cam/driver-be.js b/src2/kiri-mode/cam/driver-be.js new file mode 100644 index 00000000..e6a2a69e --- /dev/null +++ b/src2/kiri-mode/cam/driver-be.js @@ -0,0 +1,98 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { codec } from '../../kiri/codec.js'; +import { cam_slice } from './slice.js'; +import { cam_prepare } from './prepare.js'; +import { cam_export } from './export.js'; + +function surface_prep(widget, index) { + if (!widget.tool) { + let tool = widget.tool = new mesh.tool(); + let translate = index ? true : false; + tool.index(widget.getGeoVertices({ unroll: true, translate })); + } +}; + +function surface_find(widget, faces, radians) { + CAM.surface_prep(widget); + return widget.tool.findConnectedSurface(faces, radians || 0, 0.0); +}; + +function init(worker) { + worker.cam_surfaces = function(data, send) { + const { settings, index } = data; + const widgets = Object.values(worker.cache); + for (let widget of widgets) { + if (index) { + widget.setIndexed(true); + widget.setAxisIndex(-index); + } else { + widget.setIndexed(false); + widget.setAxisIndex(0); + } + CAM.surface_prep(widget, index); + } + send.done({}); + }; + + worker.cam_surface_find = function(data, send) { + const { id, face, radians } = data; + const widget = worker.cache[id]; + const faces = CAM.surface_find(widget, [face], radians); + send.done(faces); + } + + worker.cam_traces = async function(data, send) { + const { settings, single } = data; + const widgets = Object.values(worker.cache); + const fresh = []; + for (let widget of widgets) { + if (await CAM.traces(settings, widget, single)) { + fresh.push(widget); + } + } + // const fresh = widgets.filter(widget => CAM.traces(settings, widget, single)); + send.done(codec.encode(fresh.map(widget => { return { + id: widget.id, + traces: widget.traces, + } } ))); + }; + + worker.cam_traces_clear = function(data, send) { + for (let widget of Object.values(worker.cache)) { + delete widget.traces; + delete widget.topo; + } + send.done({}); + }; + + worker.cam_holes = async function(data, send) { + const { settings, indiv, rec } = data; + const widgets = Object.values(worker.cache); + const fresh = []; + + for (let [i,widget] of widgets.entries() ) { + if (await CAM.holes(settings, widget, indiv, rec, + ( prog, msg )=>{ send.data({progress: (i/widgets.length)+(prog/widgets.length),msg})} + )){ + fresh.push(widget); + } + } + + // const fresh = widgets.filter(widget => CAM.traces(settings, widget, single)); + send.done(codec.encode(fresh.map(widget => { return { + id: widget.id, + holes: widget.drills, + shadowed:widget.shadowedDrills + } } ))); + } +} + +export const CAM = { + init, + surface_prep, + surface_find, + slice: cam_slice, + prepare: cam_prepare, + export: cam_export +}; diff --git a/src2/kiri-mode/cam/driver-fe.js b/src2/kiri-mode/cam/driver-fe.js new file mode 100644 index 00000000..2b6ce455 --- /dev/null +++ b/src2/kiri-mode/cam/driver-fe.js @@ -0,0 +1,89 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../kiri/api.js'; +import { codec } from '../../kiri/codec.js'; + +function surface_prep(index, ondone) { + api.client.sync(); + const settings = api.conf.get(); + api.client.send("cam_surfaces", { settings, index }, output => { + ondone(output); + }); +}; + +function surface_show(widget) { + widget.selectFaces(Object.values(widget._surfaces).flat()); +}; + +function surface_toggle(widget, face, radians, ondone) { + let surfaces = widget._surfaces = widget._surfaces || {}; + for (let [root, faces] of Object.entries(surfaces)) { + if (faces.contains(face)) { + // delete this face group + delete surfaces[root]; + surface_show(widget); + ondone(Object.keys(surfaces).map(v => parseInt(v))); + return; + } + } + api.client.send("cam_surface_find", { id: widget.id, face, radians }, faces => { + if (faces.length) { + surfaces[face] = faces; + surface_show(widget); + } + ondone(Object.keys(surfaces).map(v => parseInt(v))); + }); +}; + +function surface_clear(widget) { + widget.selectFaces([]); + widget._surfaces = {}; +}; + +function traces(ondone, single) { + api.client.sync(); + const settings = api.conf.get(); + const widgets = api.widgets.map(); + api.client.send("cam_traces", { settings, single }, output => { + const ids = []; + codec.decode(output).forEach(rec => { + ids.push(rec.id); + widgets[rec.id].traces = rec.traces; + }); + ondone(ids); + }); +}; + +function traces_clear(ondone) { + api.client.send("cam_traces_clear", {}, () => { + // console.log({clear_traces: true}); + }); +}; + +function holes(indiv,rec,onProgress,onDone) { + api.client.sync(); + const settings = api.conf.get(); + return new Promise((res,rej)=>{ + api.client.send("cam_holes", { settings, rec, indiv }, output => { + let out = codec.decode(output) + if(out.progress != undefined){ + //if a progress message, + onProgress(out.progress,out.msg) + }else{ + api.hide.alert(alert); + onDone(out); + res(out); + } + }); + }) +}; + +export const CAM = { + surface_prep, + surface_show, + surface_toggle, + surface_clear, + traces, + traces_clear, + holes +}; diff --git a/src2/kiri-mode/cam/export.js b/src2/kiri-mode/cam/export.js new file mode 100644 index 00000000..da76a890 --- /dev/null +++ b/src2/kiri-mode/cam/export.js @@ -0,0 +1,518 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { base } from '../../geo/base.js'; + +const { util } = base; + +/** + * @returns {Array} gcode lines + */ +export function cam_export(print, online) { + const widget = print.widgets[0]; + if (!widget) return; + + console.log({ cam_export: print }); + + const { settings } = print; + const { device, tools } = settings; + + let i, + time = 0, + lines = 0, + bytes = 0, + opnum = 0, + factor = 1, + output = [], + spindle = 0, + newSpindle = 0, + toolChanges = 0, + spindleMax = device.spindleMax, + origin = settings.origin || { x: 0, y: 0, z: 0 }, + space = device.gcodeSpace ? ' ' : '', + isRML = device.gcodeFExt?.toLowerCase() === 'rml', + stripComments = device.gcodeStrip || false, + cmdToolChange = device.gcodeChange || [ "M6 T{tool}" ], + cmdSpindle = device.gcodeSpindle || [ "M3 S{speed}" ], + cmdDwell = device.gcodeDwell || [ "G4 P{time}" ], + axis = { X: 'X', Y: 'Y', Z: 'Z', A: 'A', F: 'F', R: 'R', I: 'I', J: 'J' }, + dev = settings.device, + spro = settings.process, + maxZd = spro.camFastFeedZ, + maxXYd = spro.camFastFeed, + decimals = base.config.gcode_decimals || 3, + pos = { x:null, y:null, z:null, a:undefined, f:null, t:null, emit:null }, + line, + cidx, + mode, + point, + points = 0, + lastGn = '', + rewrite_comments = false, + compact_output = false, + lasering = false, + laserOp, + stock = settings.stock || { }, + ztOff = stock.z - widget.track.top, + bounds = widget.getBoundingBox(), + zmax = stock.z, + runbox = { + max: { x:-Infinity, y:-Infinity, z:-Infinity}, + min: { x:Infinity, y:Infinity, z:Infinity} + }, + offset = { + x: -origin.x, + y: origin.y, + z: spro.camOriginTop ? origin.z - zmax : origin.z + }, + scale = { + x: 1, + y: 1, + z: 1 + }, + consts = { + box: runbox, + tool: 0, + tool_name: "unknown", + top: (offset ? dev.bedDepth : dev.bedDepth/2), + left: (offset ? 0 : -dev.bedWidth/2), + right: (offset ? dev.bedWidth : dev.bedWidth/2), + bottom: (offset ? 0 : -dev.bedDepth/2), + time_sec: 0, + time_ms: 0, + time: 0 + }; + + function section(section) { + append(); + online({ section }); + } + + function append(line) { + if (line) { + lines++; + bytes += line.length; + if (rewrite_comments) { + const spos = line.indexOf(';'); + if (spos >= 0) { + const left = line.substring(0, spos); + const right = '(' + line.substring(spos) + .replace(/; /g,'') + .replace(/;/g,'') + ')'; + line = [ left, right ].join(''); + } + } + output.append(line); + } + if (!line || output.length > 1000) { + online(output.join("\r\n")); + output = []; + } + } + + /** + * Take an array of lines and emit them after: + * - splitting a string into an array + * - stripping comments if !isRML and stripComments + * - applying constant replacements + * - applying Inch/Millimeter conversions if G20 or G21 are found + * + * @param {string|Array} array - an array of lines or a single string + * @param {Object} consts - a dictionary of constants to replace + */ + function filterEmit(array, consts) { + if (!array) { + return; + } + if (typeof(array) === 'string') { + array = array.split('\n'); + } + for (i=0; i= 0) { + line = line.substring(0, cidx).trim(); + if (line.length === 0) continue; + } + if (line.indexOf('G20') === 0) { + factor = 1/25.4; + consts.top = (offset ? dev.bedDepth : dev.bedDepth/2) * factor; + consts.left = (offset ? 0 : -dev.bedWidth/2) * factor; + consts.right = (offset ? dev.bedWidth : dev.bedWidth/2) * factor; + consts.bottom = (offset ? 0 : -dev.bedDepth/2) * factor; + } else if (line.indexOf('G21') === 0) { + factor = 1; + } + append(line); + } + } + + function add0(val, opt) { + let s = val.toString(), + d = s.indexOf("."); + if (d < 0 && decimals > 0) { + return opt ? s : s + '.0'; + } else { + return val.toFixed(decimals); + } + } + + /** + * Move to a point, applies post-processing including for laser/RML, and appends to output + * @param {Point} out.point - the point to move to + * @param {Tool} [out.tool] - the tool to move with + * @param {number} [out.speed] - the speed to move at (in mm/min) + * @param {number} [out.emit] - the emission state (0=off, 1=on) + * @param {boolean} [opt.newOp] - whether to force a new operation + * @param {number} [opt.dx] - the delta X change (if not given, uses out.point.x - pos.x) + * @param {number} [opt.dy] - the delta Y change (if not given, uses out.point.y - pos.y) + * @param {number} [opt.dz] - the delta Z change (if not given, uses out.point.z - pos.z) + * @param {number} [opt.time] - the time to move (in ms) (if not given, calculate from speed) + */ + function moveTo(out, opt = {}) { + let laser = out.type === 'laser'; + let newpos = out.point; + + // no point == dwell + // out.speed = time to dwell in ms + if (!newpos) { + time += out.speed / 60; + consts.time_sec = out.speed / 1000; + consts.time_ms = out.speed; + consts.time = consts.time_sec; + filterEmit(cmdDwell, consts); + return; + } + + newpos.x = util.round(newpos.x, decimals); + newpos.y = util.round(newpos.y, decimals); + newpos.z = util.round(newpos.z, decimals); + + // on tool change + let changeTool = !(out.tool && pos.t) || out.tool.getID() !== pos.t.getID(); + if (changeTool) { + pos.t = out.tool; + consts.tool = pos.t.getNumber(); + consts.tool_name = pos.t.getName(); + if (!laserOp && (spro.camToolInit || toolChanges > 0)) { + filterEmit(cmdToolChange, { ...consts, spindle: newSpindle } ); + } + toolChanges++; + } + + // on spindle change (deferred from layer transition so it's post-toolchange) + if (spindleMax && newSpindle && (changeTool || newSpindle !== spindle || opt.newOp)) { + spindle = newSpindle; + if (!laserOp) { + if (spindle > 0) { + let speed = Math.abs(spindle); + filterEmit(cmdSpindle, { speed, spindle: speed, rpm: speed }); + } else { + append("M4"); + } + } + } + + // enforce XY origin at start of print + if (points === 0 || changeTool) { + pos.x = pos.y = pos.z = 0; + } + + // split first move to X,Y then Z for that new location + // safety to prevent tool crashing + if (points === 0 || changeTool) { + points++; + if (spro.camFirstZMax) { + moveTo({ + // speed: Infinity, + tool: out.tool, + point: { x: pos.x, y: pos.y, z: newpos.z, a: newpos.a }, + emit: 0, + }, { + dx: 0, dy: 0, dz: 1, time: 0 + }); + moveTo({ + // speed: Infinity, + tool: out.tool, + point: { x: newpos.x, y: newpos.y, z: newpos.z, a: pos.a }, + emit: 1, + }, { + dx: 1, dy: 1, dz: 0, time: 0 + }); + } else { + moveTo({ + // speed: Infinity, + tool: out.tool, + point: { x: newpos.x, y: newpos.y, z: pos.z, a: pos.a }, + emit: 0 + }, { + dx: 1, dy: 1, dz: 0, time: 0 + }); + moveTo({ + // speed: Infinity, + tool: out.tool, + point: { x: newpos.x, y: newpos.y, z: newpos.z, a: newpos.a }, + emit: 1 + }, { + dx: 0, dy: 0, dz: 1, time: 0 + }); + } + points--; + return; + } + let gn; + if ( out.emit >=0 && out.emit <= 3) gn = `G${out.emit}`; + let speed = out.speed, + arc = out.emit == 2 || out.emit == 3, + center = out.center, + nl = (compact_output && lastGn === gn) ? [] : [gn], + dx = opt.dx || newpos.x - pos.x, + dy = opt.dy || newpos.y - pos.y, + dz = opt.dz || newpos.z - pos.z, + da = newpos.a != pos.a, + maxf = dz ? maxZd : maxXYd, + feed = Math.min(speed || maxf, maxf), + dist = Math.sqrt(dx * dx + dy * dy + dz * dz), + newFeed = feed && feed !== pos.f; + + + // drop dup points (all deltas are 0) + if (!arc && !(dx || dy || dz || da)) { + return; + } + + lastGn = gn; + + if (dx || newpos.x !== pos.x) { + pos.x = newpos.x; + runbox.min.x = Math.min(runbox.min.x, pos.x); + runbox.max.x = Math.max(runbox.max.x, pos.x); + nl.append(space).append(axis.X).append(add0(consts.pos_x = pos.x * factor)); + } + if (dy || newpos.y !== pos.y) { + pos.y = newpos.y; + runbox.min.y = Math.min(runbox.min.y, pos.y); + runbox.max.y = Math.max(runbox.max.y, pos.y); + nl.append(space).append(axis.Y).append(add0(consts.pos_y = pos.y * factor)); + } + if (dz || newpos.z !== pos.z) { + pos.z = newpos.z; + runbox.min.z = Math.min(runbox.min.z, pos.z); + runbox.max.z = Math.max(runbox.max.z, pos.z); + nl.append(space).append(axis.Z).append(add0(consts.pos_z = pos.z * factor)); + } + if (da) { + pos.a = newpos.a; + nl.append(space).append(axis.A).append(add0(consts.pos_a = pos.a * -factor)); + } + if (newFeed) { + pos.f = feed; + nl.append(space).append(axis.F).append(add0(consts.feed = feed * factor, true)); + } + if(arc){ + nl.append(space).append(axis.I).append(add0(center.x * factor, true)) + .append(space).append(axis.J).append(add0(center.y * factor, true)); + } + + // temp hack to support RML1 dialect from a file extensions trigger + if (isRML) { + if (speed) { + if (newFeed) { + append(`VS${feed};`); + } + nl = [ axis.Z, add0(pos.x * factor), ",", add0(pos.y * factor), ",", add0(pos.z * factor), ";" ]; + } else { + nl = [ "PU", add0(pos.x * factor), ",", add0(pos.y * factor), ";" ]; + } + } + + if (!lasering && laser) { + // enable laser + filterEmit(laserOp.on, consts); + } + + // if (laser && pos.emit !== out.emit) { + if (laser) { + nl.append(space).append(`S${add0(out.emit)}`); + } + + if (laser) { + pos.emit = out.emit; + } + + if (lasering && !laser) { + // disable laser + filterEmit(laserOp.off, consts); + } + + // update lasering state + lasering = laser; + + // update time calculation + time += opt.time >= 0 ? opt.time : (dist / (pos.f || 1000)) * 60; + consts.time = Math.round(time); + // if (comment && !stripComments) { + // nl.append(" ; ").append(comment); + // nl.append(" ; ").append(points); + // } + append(nl.join('')); + points++; + } + + // look for SCALE header directive + let gcodePre = []; + for (let line of device.gcodePre) { + if (line.indexOf(';; SCALE ') === 0) { + try { + let map = JSON.parse(line.substring(9)); + if (map.X) scale.x = map.X; + if (map.Y) scale.y = map.Y; + if (map.Z) scale.z = map.Z; + if (map.A) scale.a = map.A; + if (map.DEC !== undefined) decimals = parseInt(map.DEC); + console.log('export scaling applied', map); + } catch (e) { + console.log('malformed scale directive', line); + } + } else if (line.indexOf(';; REWRITE-COMMENTS-PARENS') === 0) { + rewrite_comments = true; + } else if (line.indexOf(';; DECIMALS') === 0) { + decimals = parseInt(line.split('=')[1].trim() || 3) || 3; + } else if (line.indexOf(';; COMPACT-OUTPUT') === 0) { + compact_output = true; + stripComments = true; + space = ''; + } else if (line.indexOf(";; AXISMAP ") === 0) { + let axmap = JSON.parse(line.substring(11).trim()); + for (let key in axmap) { + axis[key] = axmap[key]; + } + } else { + gcodePre.push(line); + } + } + + section('header'); + if (!stripComments) { + append(`; Generated by Kiri:Moto ${kiri.version}`); + append(`; ${new Date().toString()}`); + filterEmit(["; Bed left:{left} right:{right} top:{top} bottom:{bottom}"], consts); + append(`; Stock X:${stock.x?.round(2)} Y:${stock.y?.round(2)} Z:${stock.z?.round(2)}`); + append(`; Target: ${settings.filter[settings.mode]}`); + append("; --- process ---"); + for (let pk in spro) { + if (pk !== "ops" && pk !== "op2") { + append("; " + pk + " = " + spro[pk]); + } + } + } + + // collect tool info to add to header + let toolz = {}, ctool; + const isOffset = offset && (offset.x || offset.y); + // remap points as necessary for origins, offsets, inversions + print.output.forEach(layer => { + if (!Array.isArray(layer)) { + return; + } + layer.forEach(out => { + if (out.gcode || out.type === 'lerp') { + return; + } + if (out.tool && out.tool !== ctool) { + ctool = out.tool; + toolz[out.tool] = ctool; + } + point = out.point; + if (!point || point.mod) return; + // ensure not point is modified twice + point.mod = 1; + if (isOffset) { + point.x += offset.x; + point.y += offset.y; + } + if (scale) { + point.x *= scale.x; + point.y *= scale.y; + point.z *= scale.z; + } + if (point.a && scale.a) { + point.a *= scale.a; + } + if (spro.outputInvertX) point.x = -point.x; + if (spro.outputInvertY) point.y = -point.y; + if (spro.camOriginTop) point.z = point.z - zmax; + }); + }); + + if (!stripComments) { + // emit tools used in comments + append("; --- tools ---"); + Object.keys(toolz).sort().forEach(tn => { + let { number, flute_diam, flute_len, metric } = toolz[tn].tool; + append(`; tool#=${number} flute=${flute_diam} len=${flute_len} unit=${metric ? 'metric' : 'imperial'}`); + }); + } + // emit gcode preamble + filterEmit(gcodePre, consts); + + // emit all points in layer/point order + for (let layerout of print.output) { + const newmode = layerout.mode; + if (newmode) { + if (newmode.type === 'laser on') { + laserOp = newmode; + } else if (newmode.type === 'laser off') { + laserOp = undefined; + // force tool change in case laser tool matches next tool + pos.t = undefined; + } + } + const firstOut = layerout[0]; + const newOp = newmode && !newmode.silent && mode !== newmode; + if (newOp) { + if (mode && !stripComments) { + append("; ending " + mode.type + " op after " + Math.round(time) + " seconds"); + } + mode = newmode; + if (mode) { + section(`op-${opnum++}-${mode.type}`); + } + if (!stripComments && mode) { + append("; starting " + mode.type + " op"); + } + } + newSpindle = layerout.spindle; + // iterate over layer output records + layerout.forEach((out, ind) => { + if (out.type === 'lerp') { + // suppress display only lerp points + return; + } + if (out.gcode && Array.isArray(out.gcode)) { + filterEmit(out.gcode, consts); + } else { + moveTo(out, { newOp: ind === 0 ? newOp : false }); + } + }); + if (lasering && laserOp) { + // disable laser + filterEmit(laserOp.off, consts); + lasering = false; + } + } + + if (mode && !stripComments) { + append("; ending " + mode.type + " op after " + Math.round(time) + " seconds"); + } + + // emit gcode post + section('footer'); + filterEmit(device.gcodePost, consts); + + // flush buffered gcode + append(); + + print.time = time; + print.lines = lines; + print.bytes = bytes + lines - 1; + print.bounds = runbox; +}; diff --git a/src2/kiri-mode/cam/ops.js b/src2/kiri-mode/cam/ops.js new file mode 100644 index 00000000..6581f926 --- /dev/null +++ b/src2/kiri-mode/cam/ops.js @@ -0,0 +1,1954 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { Tool } from './tool.js'; +import { newPoint } from '../../geo/point.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { newSlice } from '../../kiri/slice.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { poly2polyEmit, tip2tipEmit, calc_normal as segmentNormal, calc_vertex as vertexNormal } from '../../geo/paths.js'; + +const DEG2RAG = Math.PI / 180; + +class CamOp { + constructor(state, op) { + this.state = state; + this.op = op + } + + type() { + return this.op.type; + } + + weight() { + return 1; + } + + async slice() { } + + prepare() { } +} + +class OpIndex extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice() { + let { op, state } = this; + if (!state.isIndexed) { + throw 'index op requires indexed stock'; + } + let { widget, updateSlicer, computeShadows, setAxisIndex } = state; + this.degrees = setAxisIndex(op.degrees, op.absolute); + // force recompute of topo + widget.topo = undefined; + updateSlicer(); + await computeShadows(); + } + + prepare(ops, progress) { + const { lastPoint, zmax, zclear, camOut, stock } = ops; + let last = lastPoint(); + if (last) { + // max point of stock corner radius when rotating (safe z when indexing) + const rzmax = (Math.max(stock.y, stock.z) * Math.sqrt(2)) / 2 + zclear; + const zmove = Math.max(rzmax, zmax); + // move above rotating stock + camOut(last = last.clone().setZ(zmove), 0); + // issue rotation command + camOut(last = last.clone().setY(0).setA(this.degrees), 0); + } + } +} + +class OpGCode extends CamOp { + constructor(state, op) { + super(state, op); + } + + prepare(ops, progress) { + ops.addGCode(this.op.gcode); + } +} + +class OpLaserOn extends CamOp { + constructor(state, op) { + super(state, op); + } + + prepare(ops, progress) { + const { printPoint, setPrintPoint, setTool, zmax, camOut } = ops; + this.op.silent = true; + setTool(0); + ops.addGCode(this.op.enable); + ops.setLasering(true, this.op.power); + } +} + +class OpLaserOff extends CamOp { + constructor(state, op) { + super(state, op); + } + + prepare(ops, progress) { + const { printPoint, zmax, camOut } = ops; + this.op.silent = true; + ops.addGCode(this.op.disable); + ops.setLasering(false); + camOut(printPoint.clone().setZ(zmax), 0); + } +} + +class OpLevel extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, widget, addSlices } = state; + let { updateToolDiams, tabs, cutTabs } = state; + let { bounds, zMax, ztOff, color, tshadow } = state; + let { stock } = settings; + + let toolDiam = new Tool(settings, op.tool).fluteDiameter(); + let stepOver = this.stepOver = toolDiam * op.step; + let z = zMax + ztOff - op.down; + + updateToolDiams(toolDiam); + + let points = []; + let clear = op.stock ? + [ newPolygon().centerRectangle({x:0,y:0,z:0}, stock.x, stock.y) ] : + POLY.outer(POLY.offset(tshadow, toolDiam * (op.over || 0))); + + POLY.fillArea(clear, 90, stepOver, points); + + let lines = this.lines = []; + for (let i=0; i { return { first: p.first(), last: p.last(), poly: p } }); + printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => { + let poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + poly.forEachPoint((point, pidx) => { + camOut(point.clone(), true, stepOver); + }, false); + }); + setPrintPoint(printPoint); + + newLayer(); + } +} + +class OpRough extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, slicer, addSlices, unsafe, color } = state; + let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state; + let { ztOff, zMax, shadowAt, isIndexed} = state; + let { workarea } = state; + let { process, stock } = settings; + + if (op.down <= 0) { + throw `invalid step down "${op.down}"`; + } + + let roughIn = op.inside; + let roughDown = op.down; + let roughLeave = op.leave || 0; + let roughLeaveZ = op.leavez || 0; + let roughStock = op.all && isIndexed; + let toolDiam = new Tool(settings, op.tool).fluteDiameter(); + let trueShadow = process.camTrueShadow === true; + + updateToolDiams(toolDiam); + + // clear the stock above the area to be roughed out + if (workarea.top_z > workarea.top_part) { + let shadow = state.shadow.base.clone(); + let step = toolDiam * op.step; + let inset = roughStock ? + POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], step) : + POLY.offset(shadow, roughIn ? step : step + roughLeave + toolDiam / 2); + let facing = POLY.offset(inset, -step, { count: 999, flat: true }); + let zdiv = ztOff / roughDown; + let zstep = (zdiv % 1 > 0) ? ztOff / (Math.floor(zdiv) + 1) : roughDown; + if (ztOff === 0) { + // compensate for lack of z top offset in this scenario + ztOff = zstep; + } + let zsteps = Math.round(ztOff / zstep); + let camFaces = this.camFaces = []; + let zstart = zMax + ztOff - zstep; + for (let z = zstart; zsteps > 0; zsteps--) { + let slice = newSlice(); + slice.z = z; + slice.camLines = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ); + slice.output() + .setLayer("face", {face: color, line: color}) + .addPolys(slice.camLines); + addSlices(slice); + camFaces.push(slice); + z -= zstep; + } + } + + // create roughing slices + let flats = []; + let shadow = []; + let slices = []; + let indices = slicer.interval(roughDown, { + down: true, min: 0, fit: true, off: 0.01 + }); + + // shift out first (top-most) slice + indices.shift(); + + // find flats and add to indices for slicing + if (op.flats) { + let flatArea = (Math.PI * (toolDiam/2) * (toolDiam/2)) / 2; + let flats = Object.entries(slicer.zFlat) + .filter(row => row[1] > flatArea) + .map(row => row[0]) + .map(v => parseFloat(v).round(5)) + .filter(v => v >= workarea.bottom_z); + flats.forEach(v => { + if (!indices.contains(v)) { + indices.push(v); + } + }); + indices = indices.sort((a,b) => { return b - a }); + // if layer is not on a flat and next one is, + // then move this layer up to mid-point to previous layer + // this is not perfect. the best method is to interpolate + // between flats so that each step is < step down. on todo list + for (let i=1; i (parseFloat(v) - 0.01).round(5)) + .filter(v => v > 0 && indices.indexOf(v) < 0); + indices = indices.appendAll(flats).sort((a,b) => b-a); + } + + indices = indices.filter(v => v >= workarea.bottom_z); + // console.log('indices', ...indices, {zBottom}); + + let cnt = 0; + let tot = 0; + await slicer.slice(indices, { each: data => { + shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); + if (flats.indexOf(data.z) >= 0) { + // exclude flats injected to complete shadow + return; + } + if (data.z > workarea.top_z) { + return; + } + data.shadow = trueShadow ? shadowAt(data.z) : shadow.clone(true); + data.slice.shadow = data.shadow; + slices.push(data.slice); + progress(0.25 + 0.25 * (++cnt / tot)); + }, progress: (index, total) => { + tot = total; + progress((index / total) * 0.25); + } }); + + if (trueShadow) { + shadow = state.shadow.base.clone(true); + } else { + shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true); + } + + // inset or eliminate thru holes from shadow + shadow = POLY.flatten(shadow.clone(true), [], true); + thruHoles.forEach(hole => { + shadow = shadow.map(p => { + if (p.isEquivalent(hole)) { + let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.05)); + return po ? po[0] : undefined; + } else { + return p; + } + }).filter(p => p); + }); + shadow = POLY.nest(shadow); + if (op.voids) { + // eliminate voids from shadow when "clear voids" enables + for (let s of shadow) s.inner = undefined; + } + + // shell = shadow expanded by half tool diameter + leave stock + const sadd = roughIn ? toolDiam / 2 : toolDiam / 2; + const shell = roughStock ? + POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], sadd) : + POLY.offset(shadow, sadd + roughLeave); + + slices.forEach((slice, index) => { + let offset = [shell.clone(true),slice.shadow.clone(true)].flat(); + let flat = POLY.flatten(offset, [], true); + let nest = POLY.setZ(POLY.nest(flat), slice.z); + + // inset offset array by 1/2 diameter then by tool overlap % + offset = POLY.offset(nest, [-(toolDiam / 2 + roughLeave), -toolDiam * op.step], { + minArea: Math.min(0.01, toolDiam * op.step / 4), + z: slice.z, + count: 999, + flat: true, + call: (polys, count, depth) => { + // used in depth-first path creation + polys.forEach(p => { + p.depth = depth; + if (p.inner) { + p.inner.forEach(p => p.depth = depth); + } + }); + } + }) || []; + + // add outside pass if not inside only + if (!roughIn && !roughStock) { + const outside = POLY.offset(shadow.clone(), toolDiam / 2 + roughLeave, {z: slice.z}); + if (outside) { + outside.forEach(p => p.depth = -p.depth); + offset.appendAll(outside); + } + } + + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + offset = cutTabs(tabs, offset, slice.z); + } + + if (!offset) return; + + if (process.camStockClipTo && stock.x && stock.y && stock.center) { + let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y); + offset = cutPolys([rect], offset, slice.z, true); + } + + // elimate double inset on inners + offset.forEach(op => { + if (op.inner) { + let pv1 = op.perimeter(); + let newinner = []; + op.inner.forEach(oi => { + let pv2 = oi.perimeter(); + let pct = pv1 > pv2 ? pv2/pv1 : pv1/pv2; + if (pct < 0.98) { + newinner.push(oi); + } + }); + op.inner = newinner; + } + }); + + slice.camLines = offset; + if (roughLeaveZ) { + // offset roughing in Z as well to minimize + // tool marks on curved surfaces + // const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2); + slice.camLines.forEach(p => { + p.setZ(p.getZ() + roughLeaveZ); + }); + } + if (false) slice.output() + .setLayer("slice", {line: 0xaaaa00}, true) + .addPolys(slice.topPolys()) + // .setLayer("top shadow", {line: 0x0000aa}) + // .addPolys(tshadow) + // .setLayer("rough shadow", {line: 0x00aa00}) + // .addPolys(shadow) + .setLayer("rough shell", {line: 0xaa0000}) + .addPolys(shell); + progress(0.5 + 0.5 * (index / slices.length)); + }); + + let last = slices[slices.length-1]; + + if (workarea.bottom_z < 0) + for (let zneg of base.util.lerp(0, -workarea.bottom_cut, op.down)) { + if (!last) continue; + let add = last.clone(true); + add.z -= zneg; + add.camLines = last.camLines.clone(true); + add.camLines.forEach(p => p.setZ(add.z + roughLeaveZ)); + // add.tops.forEach(top => top.poly.setZ(add.z)); + // add.shadow = last.shadow.clone(true); + slices.push(add); + } + + slices.forEach(slice => { + slice.output() + .setLayer("roughing", {face: color, line: color}) + .addPolys(slice.camLines); + }); + this.sliceOut = slices.filter(slice => slice.camLines); + + addSlices(this.sliceOut); + } + + prepare(ops, progress) { + let { op, state, sliceOut, camFaces } = this; + let { setTool, setSpindle, setPrintPoint, sliceOutput, polyEmit } = ops; + let { camOut, newLayer, printPoint } = ops; + let { settings } = state; + let { process } = settings; + + let easeDown = process.camEaseDown; + let cutdir = op.ov_conv; + let depthFirst = process.camDepthFirst && !state.isIndexed; + let depthData = []; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + // output the clearing of stock above roughing + for (let slice of (camFaces || [])) { + const level = []; + for (let poly of slice.camLines) { + level.push(poly); + if (poly.inner) { + poly.inner.forEach(function(inner) { + level.push(inner); + }); + } + } + // set winding specified in output + POLY.setWinding(level, cutdir, false); + poly2polyEmit(level, printPoint, (poly, index, count) => { + printPoint = polyEmit(poly, index, count, printPoint); + }); + newLayer(); + } + + // output the roughing passes + setPrintPoint(printPoint); + sliceOutput(sliceOut, { + cutdir, + depthFirst, + easeDown: op.down && easeDown ? 0.001 : 0, + progress: (n,m) => progress(n/m, "routing") + }); + } +} + +class OpOutline extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, widget, slicer, addSlices, tshadow, thruHoles, unsafe, color } = state; + let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax } = state; + let { process, stock } = settings; + + if (op.down <= 0) { + throw `invalid step down "${op.down}"`; + } + let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter(); + updateToolDiams(toolDiam); + + let shadow = []; + let slices = []; + let intopt = { + off: 0.01, + fit: true, + down: true, + min: Math.max(0, workarea.bottom_z), + max: workarea.top_z + }; + let indices = slicer.interval(op.down, intopt); + let trueShadow = process.camTrueShadow === true; + // shift out first (top-most) slice + indices.shift(); + // add flats to shadow + const flats = Object.keys(slicer.zFlat) + .map(v => (parseFloat(v) - 0.01).round(5)) + .filter(v => v > 0 && indices.indexOf(v) < 0); + indices = indices.appendAll(flats).sort((a,b) => b-a); + + let cnt = 0; + let tot = 0; + if (op.outside && !op.inside) { + // console.log({outline_bypass: indices, down: op.down}); + indices.forEach((ind,i) => { + if (flats.indexOf(ind) >= 0) { + // exclude flats + return; + } + let slice = newSlice(ind); + slice.shadow = shadow.clone(true); + slices.push(slice); + }); + } else + await slicer.slice(indices, { each: data => { + shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); + if (flats.indexOf(data.z) >= 0) { + // exclude flats injected to complete shadow + return; + } + data.shadow = trueShadow ? shadowAt(widget, data.z) : shadow.clone(true); + data.slice.shadow = data.shadow; + // data.slice.tops[0].inner = data.shadow; + // data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z); + slices.push(data.slice); + // data.slice.xray(); + // onupdate(0.2 + (index/total) * 0.1, "outlines"); + progress(0.5 + 0.5 * (++cnt / tot)); + }, progress: (index, total) => { + tot = total; + progress((index / total) * 0.5); + } }); + shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true); + + // start slices at top of stock when `clear top` enabled + if (op.top) { + let first = slices[0]; + let zlist = slices.map(s => s.z); + for (let z of indices.filter(v => v >= zMax)) { + if (zlist.contains(z)) { + continue; + } + let add = first.clone(true); + add.tops.forEach(top => top.poly.setZ(add.z)); + add.shadow = first.shadow.clone(true); + add.z = z; + slices.splice(0,0,add); + } + } + + // extend cut thru (only when z bottom is 0) + if (workarea.bottom_z < 0) { + let last = slices[slices.length-1]; + for (let zneg of base.util.lerp(0, -workarea.bottom_cut, op.down)) { + if (!last) continue; + let add = last.clone(true); + add.tops.forEach(top => top.poly.setZ(add.z)); + add.shadow = last.shadow.clone(true); + add.z -= zneg; + slices.push(add); + } + } + + slices.forEach(slice => { + let tops = slice.shadow; + + // outside only (use tshadow for entire cut) + if (op.outside) { + tops = tshadow; + } + + if (op.omitthru) { + // eliminate thru holes from shadow + for (let hole of thruHoles) { + for (let top of tops) { + if (!top.inner) continue; + top.inner = top.inner.filter(innr => { + return !innr.isEquivalent(hole, false, 0.1); + }); + } + } + } + + if (op.omitvoid) { + for (let top of tops) { + delete top.inner; + } + } + + let offset = POLY.expand(tops, toolDiam / 2, slice.z); + if (!(offset && offset.length)) { + return; + } + + // when pocket only, drop first outer poly + // if it matches the shell and promote inner polys + if (op.inside) { + let shell = POLY.expand(tops.clone(), toolDiam / 2); + offset = POLY.filter(offset, [], function(poly) { + if (poly.area() < 1) { + return null; + } + for (let sp=0; sp 0; c--){ + offset.slice().forEach(op => { + // clone removes inners but the real solution is + // to limit expanded shells to through holes + let wideCut = POLY.expand([op.clone(true)], stepover*c, slice.z, [], 1); + wideCut.forEach(cut =>{ //set order of cuts when wide + cut.order = c + if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c }) + }); + wideCuts.push(...wideCut) + }); + } + offset.appendAll(wideCuts); + } + } + + if (op.dogbones && !op.wide) { + addDogbones(offset, toolDiam / 5); + } + + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + offset = cutTabs(tabs, offset, slice.z); + } + + if (process.camStockClipTo && stock.x && stock.y && stock.center) { + let rect = newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y); + offset = cutPolys([rect], offset, slice.z, true); + } + + // offset.xout(`slice ${slice.z}`); + slice.camLines = offset; + }); + + // when top expand fails above, it creates an empty slice + slices = slices.filter(s => s.camLines); + + // project empty up and render + for (let slice of slices) { + if (false) slice.output() + .setLayer("slice", {line: 0xaaaa00}, false) + .addPolys(slice.topPolys()) + slice.output() + .setLayer("outline", {face: color, line: color}) + .addPolys(slice.camLines); + } + + addSlices(slices); + this.sliceOut = slices; + } + + prepare(ops, progress) { + let { op, state, sliceOut } = this; + let { setTool, setSpindle, setPrintPoint } = ops; + let { polyEmit, depthOutlinePath } = ops; + let { camOut, newLayer, printPoint } = ops; + let { settings, widget } = state; + let { process, controller } = settings; + + let easeDown = process.camEaseDown; + let toolDiam = this.toolDiam; + let cutdir = op.ov_conv;//process.camConventional; + let depthFirst = process.camDepthFirst; + let depthData = []; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + // printpoint becomes NaN in engine mode. not sure why but this fixes it + if(Object.values(printPoint).some(v=>Number.isNaN(v))){ + printPoint = newPoint(0,0,0); + } + + for (let slice of sliceOut) { + let polys = [], t = [], c = []; + let lines =POLY.flatten(slice.camLines) + // console.log(lines); + lines.forEach((poly)=> { + poly.order = poly.order ?? 0; + let child = poly.parent; + if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 } + if (child) c.push(poly); else t.push(poly); + poly.layer = depthData.layer; + polys.push(poly); + }); + + // set cut direction on outer polys + POLY.setWinding(t, cutdir); + // set cut direction on inner polys + POLY.setWinding(c, !cutdir); + + if (depthFirst) { + depthData.push(polys); + } else { + let orderSplit = {} + polys.forEach(poly => { + if(poly.order in orderSplit) orderSplit[poly.order].push(poly); + else orderSplit[poly.order] = [poly]; + }) + Object.entries(orderSplit) //split the polys by order + .sort((a,b) => -(a[0] - b[0] )) //sort by order (highest first) + .forEach(([order, orderPolys]) => { // emit based on closest for each order + let polyLast; + // console.log({order, orderPolys}); + printPoint = poly2polyEmit(orderPolys, printPoint, function(poly, index, count) { + + polyLast = polyEmit(poly, index, count, polyLast); + }, { swapdir: false }); + }) + newLayer(); + } + } + + if (depthFirst) { + let flatLevels = depthData.map(level => { + return POLY.flatten(level.clone(true), [], true).filter(p => !(p.depth = 0)); + }).filter(l => l.length > 0); + if (flatLevels.length && flatLevels[0].length) { + // start with the smallest polygon on the top + printPoint = flatLevels[0] + .sort((a,b) => { return a.area() - b.area() })[0] + .average(); + // experimental start of ease down + let ease = op.down && easeDown ? 0.001 : 0; + printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false, ease); + printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true, ease); + } + } + + setPrintPoint(printPoint); + } +} + +function createFilter(op) { + let filter = slices => slices; + if (op.filter) { + try { + const obj = eval(`( ${op.filter.join('\n')} )`); + let idx = 0; + if (obj && obj.slices) { + const nadd = []; + filter = function(slices) { + for (let slice of slices) { + if (obj.slices(slice, idx++)) { + nadd.push(slice); + } + } + return nadd; + }; + } + } catch (e) { + console.log('filter parse error', e, op.filter); + } + } + return filter; +} + +class OpContour extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { addSlices } = state; + let filter = createFilter(op); + // we need topo for safe travel moves when roughing and outlining + // not generated when drilling-only. then all z moves use bounds max. + // also generates x and y contouring when selected + let topo = await Topo({ + // onupdate: (update, msg) => { + onupdate: (index, total, msg) => { + progress(index / total, msg); + }, + ondone: (slices) => { + slices = filter(slices); + this.sliceOut = slices; + addSlices(slices); + }, + contour: op, + state: state + }); + // computed if set to 0 + this.tolerance = topo.tolerance; + } + + prepare(ops, progress) { + let { op, state, sliceOut } = this; + let { settings, widget } = state; + let { process } = settings; + + let { setTolerance, setTool, setSpindle, setPrintPoint } = ops; + let { camOut, polyEmit, newLayer, printPoint, lastPoint } = ops; + let { bounds, zmax } = ops; + + let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter(); + let stepover = toolDiam * op.step * 2; + let depthFirst = process.camDepthFirst; + let depthData = []; + + setTool(op.tool, op.rate, process.camFastFeedZ); + setSpindle(op.spindle); + setTolerance(this.tolerance); + + printPoint = newPoint(bounds.min.x,bounds.min.y,zmax); + + for (let slice of sliceOut) { + // ignore debug slices + if (!slice.camLines) { + continue; + } + let polys = [], poly, emit; + slice.camLines.forEach(function (poly) { + if (depthFirst) poly = poly.clone(true); + polys.push({first:poly.first(), last:poly.last(), poly:poly}); + }); + if (depthFirst) { + depthData.appendAll(polys); + } else { + printPoint = tip2tipEmit(polys, printPoint, function(el, point, count) { + poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + poly.forEachPoint(function(point, pidx) { + camOut(point.clone(), pidx > 0, stepover); + }, false); + }); + newLayer(); + } + } + + if (depthFirst) { + printPoint = tip2tipEmit(depthData, printPoint, function(el, point, count) { + let poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + poly.forEachPoint(function(point, pidx) { + camOut(point.clone(), pidx > 0, stepover); + }, false); + newLayer(); + return lastPoint(); + }); + } + + setPrintPoint(printPoint); + } +} + +class OpLathe extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { addSlices } = state; + let filter = createFilter(op); + + this.topo = await Topo4({ + op, + state, + onupdate: (pct, msg) => { + progress(pct, msg); + }, + ondone: (slices) => { + slices = filter(slices); + this.slices = slices; + addSlices(slices, false); + } + }); + } + + prepare(ops, progress) { + let { op, state, slices, topo } = this; + let { settings } = state; + + let { setTool, setSpindle, setPrintPoint } = ops; + let { camOut, newLayer, printPoint } = ops; + let { zmax } = ops; + + let toolDiam = new Tool(settings, op.tool).fluteDiameter(); + let stepover = toolDiam * op.step * 2; + let rez = topo.resolution; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + // start top center, X = 0, Y = 0 closest to 4th axis chuck + printPoint = newPoint(0, 0, zmax); + + for (let slice of slices) { + // ignore debug slices + if (!slice.camLines) { + continue; + } + + let last; + for (let path of slice.camLines) { + let latent; + path.forEachPoint((point, pidx) => { + if (last) { + const dz = Math.abs(last.z - point.z); + if (dz < rez) { + // latent point should still be included in + // preview b/c arcs would look like straight lines + latent = point.clone(); + return; + } + if (latent) { + camOut(latent, true, stepover); + latent = undefined; + } + } + camOut(last = point.clone(), pidx > 0, stepover); + }, false); + if (latent) { + camOut(latent, true, stepover); + } + } + + newLayer(); + } + + // move to safe height and reset A axis + let last = ops.lastPoint(); + let amax = (Math.round(last.a / 360) * 360).round(2); + // camOut(last = last.clone().setZ(zmax), 0); + // camOut(last = last.clone().setA(amax), 0); + newLayer(); + ops.addGCode([`G0 Z${zmax.round(2)}`,`G0 A${amax}`,"G92 A0"]); + + setPrintPoint(last); + } +} + +class OpTrace extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + const debug = false; + let { op, state } = this; + let { tool, rate, down, plunge, offset, offover, thru } = op; + let { ov_conv } = op; + let { settings, widget, addSlices, zThru, tabs, workarea } = state; + let { updateToolDiams, cutTabs, cutPolys, healPolys, color, shadowAt } = state; + let { process, stock } = settings; + let { camStockClipTo } = process; + if (state.isIndexed) { + throw 'trace op not supported with indexed stock'; + } + // generate tracing offsets from chosen features + let zTop = workarea.top_z; + let zBottom = workarea.bottom_z; + let sliceOut = this.sliceOut = []; + let areas = op.areas[widget.id] || []; + let camTool = new Tool(settings, tool); + let toolDiam = camTool.fluteDiameter(); + let toolOver = toolDiam * op.step; + let traceOffset = camTool.traceOffset() + let cutdir = ov_conv; + let polys = []; + let reContour = false; + let canRecontour = offset !== 'none' && down === 0; + let stockRect = stock.center && stock.x && stock.y ? + newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y) : undefined; + updateToolDiams(toolDiam); + + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + } + for (let arr of areas) { + let poly = newPolygon().fromArray(arr); + POLY.setWinding([ poly ], cutdir, false); + polys.push(poly); + let zs = poly.points.map(p => p.z); + let min = Math.min(...zs); + let max = Math.max(...zs); + if (max - min > 0.0001 && canRecontour) { + reContour = true; + } + } + if (false) newSliceOut(0).output() + .setLayer("polys", {line: 0xaaaa00}, false) + .addPolys(polys); + function newSliceOut(z) { + let slice = newSlice(z); + addSlices(slice); + sliceOut.push(slice); + return slice; + } + function minZ(z) { + return zBottom ? Math.max(zBottom, z - thru) : z - thru; + } + function followZ(poly) { + if (op.dogbone) { + addDogbones(poly, toolDiam / 5, !op.revbone); + } + let z = poly.getZ(); + let slice = newSliceOut(z); + slice.camTrace = { tool, rate, plunge }; + if (tabs) { + slice.camLines = cutTabs(tabs, [poly], z); + } else { + slice.camLines = [ poly ]; + } + if (camStockClipTo && stockRect) { + slice.camLines = cutPolys([stockRect], slice.camLines, z, true); + } + if (reContour) { + state.contourPolys(widget, slice.camLines); + } + slice.output() + .setLayer("trace follow", {line: color}, false) + .addPolys(slice.camLines) + } + function clearZnew(polys, z, down) { + if (down) { + // adjust step down to a value <= down that + // ends on the lowest z specified + let diff = zTop - z; + down = diff / Math.ceil(diff / down); + } + let zs = down ? base.util.lerp(zTop, z, down) : [ z ]; + let zpro = 0, zinc = 1 / (polys.length * zs.length); + for (let poly of polys) { + // newPocket(); + for (let z of zs) { + let clip = [], shadow; + shadow = shadowAt(z); + POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); + if (op.outline) { + POLY.clearInner(clip); + } + if (clip.length === 0) { + continue; + } + let slice = newSliceOut(z); + let count = 999; + slice.camTrace = { tool, rate, plunge }; + if (toolDiam) { + const offs = [ -toolDiam / 2, -toolOver ]; + POLY.offset(clip, offs, { + count, outs: slice.camLines = [], flat:true, z, minArea: 0 + }); + } else { + // when engraving with a 0 width tip + slice.camLines = clip; + } + if (tabs) { + slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); + } else { + slice.camLines = POLY.flatten(slice.camLines, null, true); + } + POLY.setWinding(slice.camLines, cutdir, false); + slice.output() + .setLayer("trace", {line: color}, false) + .addPolys(slice.camLines) + if (debug && shadow) slice.output() + .setLayer("trace shadow", {line: 0xff8811}, false) + .addPolys(shadow) + progress(zpro, "trace"); + zpro += zinc; + addSlices(slice); + } + } + } + function similar(v1, v2, epsilon = 0.01) { + return Math.abs(v1-v2) <= epsilon; + } + function centerPoly(p1, p2) { + // follow poly with most points + if (p2.length > p1.length) { + let t = p1; + p1 = p2; + p2 = t; + } + let np = newPolygon().setOpen(true); + for (let p of p1.points) { + let q = p2.findClosestPointTo(p); + np.push(p.midPointTo3D(q.point)); + } + return np; + } + function centerPolys(polys) { + // select open polys and sort by length + let ptst = polys.filter(p => p.isOpen()).sort((a,b) => b.perimeter() - a.perimeter()); + if (ptst.length < 2) { + return polys; + } + let pt = newPoint(0,0,0); + // ensure polys are ordered with start point closest to 0,0 + ptst.forEach(p => { + if (p.last().distTo2D(pt) < p.first().distTo2D(pt)) { + p.reverse(); + } + }); + let pout = polys.filter(p => p.isClosed()); + outer: for (let i=0,l=ptst.length; i p)); + return pout; + } + // connect selected segments if open and touching + polys = healPolys(polys); + // find center line for open polys spaced by tool diameter + polys = centerPolys(polys); + switch (op.mode) { + case "follow": + let routed = []; + poly2polyEmit(polys, newPoint(0,0,0), (poly, index, count, spoint) => { + routed.push(poly); + }); + let output = []; + for (let poly of POLY.nest(routed)) { + let offdist = offset !== 'none' ? offover : 0; + if (!offdist) + switch (offset) { + case "outside": offdist = traceOffset; break; + case "inside": offdist = -traceOffset; break; + } else if (offset === "inside") { + offdist = -offdist; + } + if (offdist) { + let pnew = POLY.offset([poly], offdist, { minArea: 0, open: true }); + if (pnew) { + poly = POLY.setZ(pnew, poly.getZ()); + } else { + continue; + } + } else { + poly = [ poly ]; + } + for (let pi of POLY.flatten(poly, [], true)) + if (down) { + let zto = minZ(pi.getZ()); + if (zThru && similar(zto,0)) { + zto -= zThru; + } + for (let z of base.util.lerp(zTop, zto, down)) { + output.push(pi.clone().setZ(z)); + } + } else { + if (thru) { + pi.setZ(pi.getZ() - thru); + } + output.push(pi); + } + if (!down && op.merge) { + let nest = POLY.nest(output); + let union = POLY.union(nest, 0, true); + output = POLY.flatten(union, [], true); + } + } + for (let poly of output) { + followZ(poly); + } + break; + case "clear": + const zbo = widget.track.top - widget.track.box.d; + let zmap = {}; + for (let poly of polys) { + let z = minZ(poly.minZ()); + if (offover) { + let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true }); + if (pnew) { + poly = POLY.setZ(pnew, poly.getZ()); + } else { + continue; + } + } else { + poly = [ poly ]; + } + (zmap[z] = zmap[z] || []).appendAll(poly); + } + for (let [zv, polys] of Object.entries(zmap)) { + clearZnew(polys, parseFloat(zv), down); + } + } + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings } = state; + let { setTool, setSpindle } = ops; + + setTool(op.tool, op.rate); + setSpindle(op.spindle); + for (let slice of this.sliceOut) { + ops.emitTrace(slice); + } + } +} + +class OpPocket extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + const pocket = this; + const debug = false; + let { op, state } = this; + let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op; + let { ov_botz, ov_conv } = op; + let { settings, widget, addSlices, zBottom, zThru, tabs, color } = state; + let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state; + let { process } = settings; + zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom; + // generate tracing offsets from chosen features + let sliceOut; + let pockets = this.pockets = []; + let camTool = new Tool(settings, tool); + let toolDiam = camTool.fluteDiameter(); + let toolOver = toolDiam * op.step; + let cutdir = ov_conv; + let engrave = contour && op.engrave; + let zTop = workarea.top_z; + if (contour) { + down = 0; + this.topo = await Topo({ + // onupdate: (update, msg) => { + onupdate: (index, total, msg) => { + progress((index / total) * 0.9, msg); + }, + ondone: (slices) => { + // console.log({ contour: slices }); + }, + contour: { + tool, + tolerance, + inside: true, + axis: "-" + }, + state: state + }); + } + updateToolDiams(toolDiam); + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + } + function newPocket() { + pockets.push(sliceOut = []); + } + function newSliceOut(z) { + let slice = newSlice(z); + sliceOut.push(slice); + return slice; + } + function clearZ(polys, z, down) { + if (down) { + // adjust step down to a value <= down that + // ends on the lowest z specified + let diff = zTop - z; + down = diff / Math.ceil(diff / down); + } + let zs = down ? base.util.lerp(zTop, z, down) : [ z ]; + if (engrave) { + toolDiam = toolOver; + } + if (contour) { + expand = engrave ? 0 : expand; + } else if (expand) { + polys = POLY.offset(polys, expand); + } + let zpro = 0, zinc = 1 / (polys.length * zs.length); + for (let poly of polys) { + newPocket(); + for (let z of zs) { + let clip = [], shadow; + if (contour) { + if (smooth) { + clip = POLY.offset(POLY.offset([ poly ], smooth), -smooth); + } else { + clip = [ poly ]; + } + } else { + shadow = shadowAt(z); + if (smooth) { + shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smooth), -smooth), z); + } + POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); + if (op.outline) { + POLY.clearInner(clip); + } + } + if (clip.length === 0) { + continue; + } + let slice = newSliceOut(z); + let count = engrave ? 1 : 999; + slice.camTrace = { tool, rate, plunge }; + if (toolDiam) { + const offs = contour ? + [ expand || (-0.02), -toolOver ] : + [ -toolDiam / 2, -toolOver ]; + POLY.offset(clip, offs, { + count, outs: slice.camLines = [], flat:true, z, minArea: 0 + }); + } else { + // when engraving with a 0 width tip + slice.camLines = clip; + } + if (tabs) { + slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); + } else { + slice.camLines = POLY.flatten(slice.camLines, null, true); + } + POLY.setWinding(slice.camLines, cutdir, false); + if (contour) { + slice.camLines = pocket.conform(slice.camLines, op.refine, engrave, pct => { + progress(0.9 + (zpro + zinc * pct) * 0.1, "conform"); + }); + } + slice.output() + .setLayer("pocket", {line: color}, false) + .addPolys(slice.camLines) + if (debug && shadow) slice.output() + .setLayer("pocket shadow", {line: 0xff8811}, false) + .addPolys(shadow) + if (!contour) { + progress(zpro, "pocket"); + } + zpro += zinc; + addSlices(slice); + } + } + } + let surfaces = op.surfaces[widget.id] || []; + let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); + // let vert = widget.getVertices().array.map(v => v.round(4)); + let outline = []; + let faces = surface_find(widget, surfaces, (op.follow || 5) * DEG2RAG); + let zmin = Infinity; + let j=0, k=faces.length; + for (let face of faces) { + let i = face * 9; + outline.push(newPolygon() + .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) + .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) + .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) + ); + } + zmin = Math.max(zBottom, zmin); + outline = POLY.union(outline, 0.0001, true); + outline = POLY.setWinding(outline, cutdir, false); + outline = healPolys(outline); + if (smooth) { + outline = POLY.offset(POLY.offset(outline, smooth), -smooth); + } + if (outline.length) { + // option to skip interior features (holes, pillars) + if (op.outline) { + POLY.clearInner(outline); + } + if (debug) newSliceOut(zmin).output() + .setLayer("pocket area", {line: 0x1188ff}, false) + .addPolys(outline) + clearZ(outline, zmin + 0.0001, down); + progress(1, "pocket"); + } + } + + // mold cam output lines to the surface of the topo offset by tool geometry + conform(camLines, refine, engrave, progress) { + const topo = this.topo; + // re-segment polygon to a higher resolution + const hirez = camLines.map(p => p.segment(topo.tolerance * 2)); + // walk points and offset from surface taking into account tool geometry + let steps = hirez.length; + let iter = 0; + for (let poly of hirez) { + for (let point of poly.points) { + point.z = engrave ? topo.zAtXY(point.x, point.y) : topo.toolAtXY(point.x, point.y); + } + progress((iter++ / steps) * 0.8); + } + steps = steps * refine; + iter = 0; + // walk points noting z deltas and smoothing z sawtooth patterns + for (let j=0; j p.midpoints(topo.tolerance * 8)); + return hirez; + } + + prepare(ops, progress) { + let { op, state, pockets } = this; + let { setTool, setSpindle, setTolerance, sliceOutput, getPrintPoint } = ops; + let { process } = state.settings; + + setTool(op.tool, op.rate); + setSpindle(op.spindle); + + if (this.topo) { + setTolerance(this.topo.tolerance); + } + + // eliminate empty pockets + pockets = pockets.filter(p => p.length); + + // pockets is an [ array of an [ array of slices ] ] + // each top level array is a pocket containing a [ z layer array of slices ] + // follow each pocket to the next closest one from previous exit + for (;;) { + let printPoint = getPrintPoint(); + let min = { + dist: Infinity, + pocket: undefined + }; + for (let pocket of pockets.filter(p => !p.used)) { + let poly = pocket[0].camLines.slice().sort((a,b) => b.area() - a.area())[0]; + if (!poly) continue; + let find = poly.findClosestPointTo(printPoint); + if (find.distance < min.dist) { + min.pocket = pocket; + min.dist = find.distance; + } + } + if (min.pocket) { + min.pocket.used = true; + sliceOutput(min.pocket, { + cutdir: op.ov_conv, + depthFirst: process.camDepthFirst && !state.isIndexed, + easeDown: op.down && process.easeDown ? op.down : 0, + progress: (n,m) => progress(n/m, "pocket") + }); + } else { + break; + } + } + } +} + +class OpDrill extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, addSlices, widget, updateToolDiams } = state; + let { zBottom, zThru, thruHoles, color } = state; + let { drills, drillThrough} = op + + let drillTool = new Tool(settings, op.tool), + drillToolDiam = drillTool.fluteDiameter(), + sliceOut = this.sliceOut = []; + + updateToolDiams(drillToolDiam); + + const allDrills = drills[widget.id] ?? [] + // drill points to use center (average of all points) of the polygon + allDrills.forEach((drill)=> { + if(!drill.selected){ + return + } + let slice = newSlice(0); + if (op.mark) { + // replace depth with single down peck + drill.depth = op.down + } + drill.zBottom = drill.z - drill.depth; + // for thru holes, follow z thru when set + if ((op.thru>0) ) { + drill.zBottom -= op.thru; + } + const poly = newPolygon() + poly.points.push(newPoint(drill.x, drill.y, drill.z)) + poly.points.push(newPoint(drill.x, drill.y, drill.zBottom)) + // poly.points.pop(); + slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate }; + slice.camLines = [poly]; + + slice.output() + .setLayer("drill", {face: color, line: color}) + .addPolys(slice.camLines); + addSlices(slice); + sliceOut.push(slice); + }); + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings, widget, addSlices, updateToolDiams } = state; + let { setTool, setSpindle, setDrill, emitDrills } = ops; + setTool(op.tool, undefined, op.rate); + setDrill(op.down, op.lift, op.dwell,op.thru); + setSpindle(op.spindle); + emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); + } +} + +class OpRegister extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, widget, bounds, addSlices, zMax, zThru, color } = state; + let { updateToolDiams } = state; + + let tool = new Tool(settings, op.tool); + let sliceOut = this.sliceOut = []; + + updateToolDiams(tool.fluteDiameter()); + + let { stock } = settings, + tz = widget.track.pos.z, + lx = bounds.min.x, + hx = bounds.max.x, + ly = bounds.min.y, + hy = bounds.max.y, + o3 = tool.fluteDiameter() * 2, + mx = (lx + hx) / 2, + my = (ly + hy) / 2, + mz = op.thru || zThru || 0, + dx = (stock.x - (hx - lx)) / 4, + dy = (stock.y - (hy - ly)) / 4, + dz = stock.z, + points = [], + wo = stock.z - bounds.max.z, + z1 = bounds.max.z + wo + tz, + z2 = tz - mz; + + if (!(stock.x && stock.y && stock.z)) { + return; + } + + switch (op.axis) { + case "X": + case "x": + if (op.points == 3) { + points.push(newPoint(lx - dx, my, 0)); + points.push(newPoint(hx + dx, my - o3, 0)); + points.push(newPoint(hx + dx, my + o3, 0)); + } else { + points.push(newPoint(lx - dx, my, 0)); + points.push(newPoint(hx + dx, my, 0)); + } + break; + case "Y": + case "y": + if (op.points == 3) { + points.push(newPoint(mx, ly - dy, 0)); + points.push(newPoint(mx - o3, hy + dy, 0)); + points.push(newPoint(mx + o3, hy + dy, 0)); + } else { + points.push(newPoint(mx, ly - dy, 0)); + points.push(newPoint(mx, hy + dy, 0)); + } + break; + case "-": + let o2 = o3 / 2, + x0 = lx - dx, + x1 = hx + dx, + y0 = ly - dy - o2, + y1 = hy + dy + o2, + x4 = (x1 - x0 - o2) / 4, + y4 = (y1 - y0 - o2 * 3) / 4, + poly, cp, cz; + function start(z) { + cz = z; + cp = {x:x0 + o2 * 0.5, y:y0 + o2 * 1.5}; + poly = newPolygon().add(cp.x, cp.y, z); + } + function move(dx, dy) { + cp.x += dx; + cp.y += dy; + poly.add(cp.x, cp.y, cz); + } + function rept(count, tv, fn) { + while (count-- > 0) { + fn(tv, count === 0); + tv = -tv; + } + } + for (let z of base.util.lerp(z1, z2, op.down)) { + let slice = newSlice(z); + addSlices(slice); + sliceOut.push(slice); + start(z); + rept(4, o2, oy => { + move(0, -oy); + move(x4, 0); + }); + rept(4, o2, ox => { + move(ox, 0); + move(0, y4); + }); + rept(4, o2, oy => { + move(0, oy); + move(-x4, 0); + }); + rept(4, o2, ox => { + move(-ox, 0); + move(0, -y4); + }); + poly.points.pop(); + slice.camTrace = { tool: tool.getID(), rate: op.feed, plunge: op.rate }; + slice.camLines = [ poly ]; + slice.output() + .setLayer("register", {line: color}, false) + .addPolys(slice.camLines) + } + break; + } + + if (points.length) { + let slice = newSlice(0,null), polys = []; + points.forEach(point => { + polys.push(newPolygon() + .append(point.clone().setZ(z1)) + .append(point.clone().setZ(z2))); + }); + slice.camLines = polys; + slice.output() + .setLayer("register", {face: color, line: color}) + .addPolys(polys); + addSlices(slice); + sliceOut.push(slice); + } + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings, widget, addSlices, updateToolDiams } = state; + let { setTool, setSpindle, setDrill, emitDrills } = ops; + + if (op.axis === '-') { + setTool(op.tool, op.feed, op.rate); + setSpindle(op.spindle); + for (let slice of this.sliceOut) { + ops.emitTrace(slice); + } + } else { + setTool(op.tool, undefined, op.rate); + setDrill(op.down, op.lift, op.dwell); + setSpindle(op.spindle); + emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); + } + } +} + +class OpXRay extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { widget, addSlices } = this.state; + let slicer = new kiri.cam_slicer(widget); + let xrayind = Object.keys(slicer.zLine) + .map(v => parseFloat(v).round(5)) + .sort((a,b) => a-b); + let xrayopt = { each: data => { + let slice = newSlice(data.z); + slice.addTops(data.tops); + // data.tops.forEach(top => slice.addTop(top)); + slice.lines = data.lines; + slice.xray(); + addSlices(slice); + }, over: false, flatoff: 0, edges: true, openok: true }; + await slicer.slice(xrayind, xrayopt); + // xrayopt.over = true; + // slicer.slice(xrayind, xrayopt); + } +} + +/** + * Computes the Part "shadow" and attaches relevant data to the "state" object + * + * The part shadow consists of top-cown layers at which the polygon shadow changes + * shape. For curved or sloped surfaces, this is approximated and paths that clip + * to it should use the next lower layer from current Z to ensure no part collisions. + * + * The shadow at each layer is computed by top-down unioning the part outline with + * the shadow from the layer above. + * + * This operation is injected at the start of the operation chain before processing. + */ +class OpShadow extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let state = this.state; + let { ops, slicer, widget, unsafe, addSlices, shadowAt } = state; + + let realOps = ops.map(rec => rec.op).filter(op => op); + let trueShadow = state.settings.process.camTrueShadow === true; + + let minStepDown = realOps + .map(op => (op.down || 3) / (trueShadow ? 1 : 3)) + .reduce((a,v) => Math.min(a, v, 1)); + + let tslices = []; + let tshadow = []; + let tzindex = slicer.interval(minStepDown, { + fit: true, off: 0.01, down: true, flats: true + }); + let skipTerrain = unsafe; + + if (skipTerrain) { + console.log("skipping terrain generation"); + tzindex = [ tzindex.pop() ]; + } + + let lsz; // only shadow up to bottom of last shadow for progressive union + let cnt = 0; + let tot = 0; + + // terrain is the "shadow stack" where index 0 = top of part + // thus array.length -1 = bottom of part + let terrain = await slicer.slice(tzindex, { each: data => { + let shadow = trueShadow ? shadowAt(data.z, lsz) : []; + tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadow), 0.01, true); + tslices.push(data.slice); + // capture current shadow for this slice + data.slice.shadow = tshadow; + if (false) { + const slice = data.slice; + addSlices(slice); + slice.output() + .setLayer("shadow", {line: 0x888800, thin: true }) + .addPolys(POLY.setZ(tshadow.clone(true), data.z), { thin: true }); + slice.output() + .setLayer("slice", {line: 0x886622, thin: true }) + .addPolys(POLY.setZ(data.tops.clone(true), data.z), { thin: true }); + // let p1 = [], p2 = [], cp = p1; + // for (let line of data.lines) { + // cp.push(line.p1); + // cp.push(line.p2); + // cp = (cp === p1 ? p2 : p1); + // } + // slice.output() + // .setLayer("lines1", {line: 0x884444, thin: true }) + // .addLines(p1, { thin: true }); + // slice.output() + // .setLayer("lines2", {line: 0x444488, thin: true }) + // .addLines(p2, { thin: true }); + } + lsz = data.z; + progress(0.5 + 0.5 * (++cnt / tot)); + }, progress: (index, total) => { + tot = total; + progress((index / total) * 0.5); + } }); + + if (terrain.length === 0) { + throw `invalid widget shadow`; + } + + // TODO: deprecate use of separate shadow vars in state + state.center = tshadow[0].bounds.center(); + state.tshadow = tshadow; // true shadow (base of part) + state.terrain = terrain; // stack of shadow slices stored in tops + state.tslices = tslices; // raw slicer 'data' layer outputs + state.skipTerrain = skipTerrain; + + // TODO: refactor ops to use a unified shadow object + state.shadow = { + base: tshadow, // computed shadow union at base of part + stack: terrain, // stack of shadow slices + slices: tslices, // raw slicer 'data' objects + skip: skipTerrain + }; + + // identify through holes which are inner/child polygons + // on the bottom-most layer of the shadow stack (tshadow, index == 0) + state.thruHoles = tshadow.map(p => p.inner || []).flat(); + } +} + +// union triangles > z (opt cap < ztop) into polygon(s) +export function shadowAt(widget, z, ztop) { + const geo = widget.cache.geo; + const length = geo.length; + // cache faces with normals up + if (!widget.cache.shadow) { + const faces = []; + for (let i=0, ip=0; i 0.001) { + faces.push(a,b,c); + // faces.push(newPoint(...a), newPoint(...b), newPoint(...c)); + } + } + widget.cache.shadow = faces; + } + const found = []; + const faces = widget.cache.shadow; + const { checkOverUnderOn, intersectPoints } = self.kiri.cam_slicer; + for (let i=0; i ztop && b.z > ztop && c.z > ztop) { + // skip faces over top threshold + continue; + } + if (a.z < z && b.z < z && c.z < z) { + // skip faces under threshold + continue; + } else if (a.z > z && b.z > z && c.z > z) { + found.push([a,b,c]); + } else { + // check faces straddling threshold + const where = { under: [], over: [], on: [] }; + checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where); + checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where); + checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where); + if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) { + // compute two point intersections and construct line + let line = intersectPoints(where.over, where.under, z); + if (line.length === 2) { + if (where.over.length === 2) { + found.push([where.over[1], line[0], line[1]]); + found.push([where.over[0], where.over[1], line[0]]); + } else { + found.push([where.over[0], line[0], line[1]]); + } + } else { + console.log({msg: "invalid ips", line: line, where: where}); + } + } + } + } + + // const lines = {}; + // function addline(p1, p2) { + // let key = p1.key < p2.key ? p1.key + ',' + p2.key : p2.key + ',' + p1.key; + // let rec = lines[key]; + // if (rec) { + // rec.count++; + // } else { + // lines[key] = { p1, p2, count: 1 }; + // } + // } + // for (let face of found) { + // addline(face[0], face[1]); + // addline(face[1], face[2]); + // addline(face[2], face[0]); + // } + // const singles = Object.entries(lines).filter(a => a[1].count === 1).map(a => a[1]); + // const loops = POLY.nest(sliceConnect(singles, z)); + + let polys = found.map(a => { + return newPolygon() + .add(a[0].x,a[0].y,a[0].z) + .add(a[1].x,a[1].y,a[1].z) + .add(a[2].x,a[2].y,a[2].z); + }); + polys = POLY.union(polys, 0.001, true); + // console.log({z, loops, polys}); + // return loops; + return polys; +}; + +export const ops = { + "xray": OpXRay, + "shadow": OpShadow, + "level": OpLevel, + "rough": OpRough, + "outline": OpOutline, + "contour": OpContour, + "pocket": OpPocket, + "lathe": OpLathe, + "trace": OpTrace, + "drill": OpDrill, + "register": OpRegister, + "laser on": OpLaserOn, + "laser off": OpLaserOff, + "gcode": OpGCode, + "index": OpIndex, + "flip": CamOp +}; diff --git a/src2/kiri-mode/cam/prepare.js b/src2/kiri-mode/cam/prepare.js new file mode 100644 index 00000000..d172f776 --- /dev/null +++ b/src2/kiri-mode/cam/prepare.js @@ -0,0 +1,1099 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { base, util } from '../../geo/base.js'; +import { tip2tipEmit, poly2polyEmit, arcToPath } from '../../geo/paths.js'; +import { newPoint } from '../../geo/point.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { render } from '../../kiri/render.js'; +import { newPrint } from '../../kiri/print.js'; +import { Tool } from './tool.js'; + +const { toRadians } = util + +/** + * DRIVER PRINT CONTRACT + * + * @param {Object} print state object + * @param {Function} update incremental callback + * @param {Number} [index] into widget array + * @param {Object} [firstPoint] starting point + */ +export async function cam_prepare(widall, settings, update) { + const widgets = widall.filter(w => !w.track.ignore && !w.meta.disabled); + const count = widgets.length; + const weight = 1/count; + const print = self.kiri_worker.current.print = newPrint(settings, widgets); + print.output = []; + + let point; + widgets.forEach((widget, index) => { + point = prepEach(widget, settings, print, point, (progress, msg) => { + update((index * weight + progress * weight) * 0.75, msg || "prepare"); + }); + }); + + const output = print.output.filter(level => Array.isArray(level)); + + if (render) // allows it to run from CLI + return render.path( + output, + (progress, layer) => { + update(0.75 + progress * 0.25, "render", layer); + }, { + thin: true, + print: 0, + move: 0x557799, + speed: false, + moves: true, + other: "moving", + action: "milling", + maxspeed: settings.process.camFastFeed || 6000 + } + ); +}; + +export function prepEach(widget, settings, print, firstPoint, update) { + + if (widget.camops.length === 0 || widget.meta.disabled) return; + + let device = settings.device, + process = settings.process, + isIndexed = process.camStockIndexed, + startCenter = process.camOriginCenter, + alignTop = settings.controller.alignTop, + stock = settings.stock || {}, + stockz = stock.z * (isIndexed ? 0.5 : 1), + outer = settings.bounds || widget.getPositionBox(), + outerz = outer.max.z, + slices = widget.slices, + zclear = (process.camZClearance || 1), + zmax_force = process.camForceZMax || false, + zmax_outer = stockz + zclear, + wztop = widget.track.top, + ztOff = stockz - wztop, + bounds = widget.getBoundingBox(), + boundsz = isIndexed ? stock.z / 2 : bounds.max.z + ztOff, + zadd = !isIndexed ? stock.z - boundsz : alignTop ? outerz - boundsz : 0, + zmax = outerz + zclear + (process.camOriginOffZ || 0), + wmpos = widget.track.pos, + wmx = wmpos.x, + wmy = wmpos.y, + originx = (startCenter ? 0 : -stock.x / 2) + (process.camOriginOffX || 0), + originy = (startCenter ? 0 : -stock.y / 2) + (process.camOriginOffY || 0), + origin = newPoint(originx + wmx, originy + wmy, zmax), + output = print.output, + easeDown = process.camEaseDown, + easeAngle = process.camEaseAngle, + depthFirst = process.camDepthFirst, + engageFactor = process.camFullEngage, + arcTolerance = process.camArcTolerance, + arcRes = toRadians(process.camArcResolution), + tolerance = 0, + drillDown = 0, + drillLift = 0, + drillDwell = 0, + drillThru = 0, + lasering = false, + laserPower = 0, + newOutput = print.output || [], + layerOut = [], + printPoint, + isNewMode, + isPocket, + isContour, + isRough, + isLathe, + isIndex, + tool, + toolType, + toolDiam, + toolDiamMove, + plungeRate = process.camFastFeedZ, + feedRate, + lastTool, + lastPush, + lastPoint, + currentOp, + nextIsMove = true, + synthPlunge = false, + spindle = 0, + spindleMax = device.spindleMax, + maxToolDiam = widget.maxToolDiam, + terrain = widget.terrain ? widget.terrain.map(data => { + return { + z: data.z, + tops: data.tops, + }; + }) : zmax; + + function newLayer(op) { + if (layerOut.length || layerOut.mode) { + newOutput.push(layerOut); + } + layerOut = []; + layerOut.mode = op || currentOp; + layerOut.spindle = spindle; + } + + function addGCode(text) { + if (!(text && text.length)) { + return; + } + if (!Array.isArray(text)) { + text = text.trim().split('\n'); + } + newOutput.push([{ gcode: text }]); + if (layerOut.length) { + layerOut = []; + layerOut.mode = currentOp; + layerOut.spindle = spindle; + } + } + + // non-zero means contouring + function setTolerance(dist) { + tolerance = dist; + if (isContour) { + // avoid moves to safe Z when contouring short steps + toolDiamMove = currentOp.step * toolDiam * 1.5; + } + } + + function setPrintPoint(point) { + ops.printPoint = printPoint = point; + } + + function setSpindle(speed) { + spindle = Math.min(speed, spindleMax); + } + + function setTool(toolID, feed, plunge) { + if (toolID !== lastTool) { + tool = new Tool(settings, toolID); + toolType = tool.getType(); + toolDiam = tool.fluteDiameter(); + toolDiamMove = toolType === 'endmill' ? toolDiam : tolerance * 2; + lastTool = toolID; + } + feedRate = feed || feedRate || plunge; + plungeRate = Math.min(feedRate || plunge, plunge || plungeRate || feedRate); + } + + function setLasering(bool, power = 0) { + lasering = bool ? currentOp : undefined; + laserPower = power; + } + + function setDrill(down, lift, dwell, thru) { + drillDown = down; + drillLift = lift; + drillDwell = dwell; + drillThru = thru; + } + + function emitDrills(polys) { + polys = polys.slice(); + for (;;) { + let closestDist = Infinity, + closestI, + closest = null, + dist; + + for (let i=0; i down * 2) { + points.push(point.clone()); + point.z -= down; + remain -= down; + } else if (remain < down) { + points.push(point.clone()); + point.z -= remain; + points.push(point.clone()); + break; + } else { + points.push(point.clone()); + point.z -= remain / 2; + points.push(point.clone()); + point.z -= remain / 2; + points.push(point.clone()); + break; + } + } + camOut(point.clone().setZ(zmax)); + points.forEach(function(point, index) { + camOut(point, 1); + if (index > 0 && index < points.length - 1) { + if (dwell) camDwell(dwell); + if (lift) camOut(point.clone().setZ(point.z + lift), 0); + } + }) + camOut(point.clone().setZ(zmax),0); + newLayer(); + } + + /** + * @param {Point} point + * @param {number} emit (0=move, 1=/laser on/cut mode, 2/3= G2/G3 arc) + * @param {number} [speed] feed/plunge rate in mm/min + * @param {number} [tool] tool number + */ + function layerPush(point, emit, speed, tool, options) { + const { type, center, arcPoints } = options ?? {}; + const dz = (point && lastPush && lastPush.point) ? point.z - lastPush.point.z : 0; + if (dz < 0 && speed > plungeRate) { + speed = plungeRate; + } + layerOut.mode = currentOp; + if (lasering) { + let power = emit ? laserPower : 0; + if (emit && lasering.adapt) { + let { minz, maxz, minp, maxp, adaptrp } = lasering; + maxz = maxz || wztop; + let deltaz = maxz - minz; + let { z } = point; + if (adaptrp) { + while (z > maxz) z -= deltaz; + while (z < minz) z += deltaz; + } else if (z < minz || z > maxz) { + // skip outside of band + return point; + } + z -= minz; + if (minp < maxp) { + power = minp + (z / deltaz) * (maxp - minp); + } else { + power = minp - (z / deltaz) * (minp - maxp); + } + } + if (lasering.flat) { + point.z = (stock && stock.z ? stock.z : wztop) + lasering.flatz; + } + print.addOutput(layerOut, point, power, speed, tool, {type:'laser'}); + } else { + print.addOutput(layerOut, point, emit, speed, tool, {type, center, arcPoints}); + } + lastPush = { point, emit, speed, tool }; + return point; + } + + function camDwell(time) { + layerPush( + null, + 0, + time, + tool + ); + } + + /** + * Move a point by the widget's movement offset. + * @param {Point} p - point to move + * @return {Point} new point with offset applied + */ + function applyWidgetMovement(p){ + return newPoint( + p.x + wmx, + p.y + wmy, + p.z + zadd + ) + } + + /** + * emit a cut, arc, or move operation from the current location to a new location + * @param {Point} point destination for move + * @param {0|1|2|3} emit G0, G1, G2, G3 + * @param {number} opts.radius arc radius; truthy values for arc move + * @param {boolean} opts.clockwise arc direction + * @param {number} opts.moveLen typically = tool diameter used to trigger terrain detection + * @param {number} opts.factor speed scale factor + */ + function camOut(point, emit,opts) { + + // console.log({point, emit, opts}) + let { + center = {}, + clockwise = true, + arcPoints = [], + moveLen = toolDiamMove, + factor = 1, + } = opts ?? {} + + // console.log({radius}) + const isArc = emit == 2 || emit == 3; + + //apply widget movement pos + point = applyWidgetMovement(point); + + // console.log(point.z); + if (nextIsMove) { + emit = 0; + nextIsMove = false; + } + + let rate = feedRate * factor; + + // carry rotation forward when not overridden + if (point.a === undefined && lastPoint) { + point.a = lastPoint.a; + } else if (lastPoint && point.a !== undefined && lastPoint.a !== undefined) { + let DA = lastPoint.a - point.a; + let MZ = Math.max(lastPoint.z, point.z) + // find arc length + let AL = (Math.abs(DA) / 360) * (2 * Math.PI * MZ); + if (AL >= 1) { + let lerp = base.util.lerp(lastPoint.a, point.a, 1); + // create interpolated point set for rendering and animation + // console.log({ DA, MZ, AL }, lerp.length); + for (let a of lerp) { + let lp = point.clone().setA(a); + // console.log(lp.a, lp.x, lp.y, lp.z); + lastPoint = layerPush( + lp, + emit, + rate, + tool, + {type:"lerp"}, + ); + } + } + } + + // before first point, move cutting head to point above it + // then set that new point as the lastPoint + if (!lastPoint) { + let above = point.clone().setZ(stockz + zclear); + // let above = point.clone().setZ(zmax + zadd + ztOff); + lastPoint = layerPush(above, 0, 0, tool); + } + + // measure deltas to last point in XY and Z + let deltaXY = lastPoint.distTo2D(point), + deltaZ = point.z - lastPoint.z, + absDeltaZ = Math.abs(deltaZ), + isMove = emit == 0; + + // drop points too close together + if (!isLathe && !isArc && deltaXY < 0.001 && point.z === lastPoint.z) { + // console.trace(["drop dup",lastPoint,point]); + return; + } + + // convert short planar moves to cuts in some cases + if (!isRough&& !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering ) { + let iscontour = tolerance > 0; + let isflat = absDeltaZ < 0.001; + // restrict this to contouring + if (isflat || (iscontour && absDeltaZ <= tolerance)) { + emit = 1; + isMove = false; + } else if (deltaZ <= -tolerance) { + // move over before descending + layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool); + // new pos for plunge calc + deltaXY = 0; + } + } else if (isMove && isLathe) { + if (point.z > lastPoint.z) { + layerPush(lastPoint.clone().setZ(point.z), 0, 0, tool); + } else if (point.z < lastPoint.z) { + layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool); + } + } else if (isMove) { + // for longer moves, check the terrain to see if we need to go up and over + const bigXY = (deltaXY > moveLen && !lasering); + const bigZ = (deltaZ > toolDiam/2 && deltaXY > tolerance); + const midZ = (tolerance && absDeltaZ >= tolerance) && !isContour; + + if (bigXY || bigZ || midZ) { + let maxz = getZClearPath( + terrain, + lastPoint.x - wmx, + lastPoint.y - wmy, + point.x - wmx, + point.y - wmy, + Math.max(point.z, lastPoint.z), + zadd, + maxToolDiam/2, + zclear + ), + maxZdelta = Math.max(maxz - point.z, maxz - lastPoint.z), + mustGoUp = maxZdelta >= tolerance, + clearz = maxz; + let zIsBelow = point.z <= maxz; + if (zmax_force) { + clearz = maxz = zmax + zadd; + zIsBelow = true; + } + // up if any point between higher than start/outline, go up first + if (mustGoUp || zIsBelow) { + const zClearance = clearz + (isIndexed ? 0 : ztOff); + if (zIsBelow) { + layerPush(lastPoint.clone().setZ(zClearance), 0, 0, tool); + } + layerPush(point.clone().setZ(zClearance), 0, 0, tool); + // new pos for plunge calc + deltaXY = 0; + } + } else if (isRough && deltaZ < 0) { + layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool); + } + } + + // set new plunge rate + if (!lasering && deltaZ < -tolerance && !isLathe) { + let threshold = Math.min(deltaXY / 2, absDeltaZ), + modifier = threshold / absDeltaZ; + if (synthPlunge && threshold && modifier && deltaXY > tolerance) { + // use modifier to speed up long XY move plunge rates + // console.log('modifier', modifier); + rate = Math.round(plungeRate + ((feedRate - plungeRate) * modifier)); + cut = 1; + } else { + rate = 1 / Math.hypot(deltaXY / feedRate , absDeltaZ / plungeRate ); + } + } + + if (isArc) { + layerOut.mode = currentOp; + layerOut.spindle = spindle; + lastPoint = layerPush( + point, + clockwise ? 2 : 3, + rate, + tool, + { + center, + arcPoints + } + ); + } else { + // for g1 moves + // TODO: synthesize move speed from feed / plunge accordingly + layerOut.mode = currentOp; + layerOut.spindle = spindle; + lastPoint = layerPush( + point, + emit, + rate, + tool + ); + } + } + + /** + * output an array of slices that form a pocket + * used by rough and pocket ops, does not support arcs + * + * @param {Slice[]} top-down Z stack of slices + * @param {*} opts + */ + function sliceOutput(sliceOut, opts = {}) { + const { cutdir, depthFirst, easeDown, progress } = opts; + + let total = 0; + let depthData = []; + + for (let slice of sliceOut) { + let polys = [], t = [], c = []; + POLY.flatten(slice.camLines).forEach(function (poly) { + let child = poly.parent; + if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 } + if (child) c.push(poly); else t.push(poly); + poly.layer = depthData.layer; + polys.push(poly); + }); + + // set cut direction on outer polys + POLY.setWinding(t, cutdir); + // set cut direction on inner polys + POLY.setWinding(c, !cutdir); + + if (depthFirst) { + depthData.push(polys); + } else { + // if not depth first, output the polys in slice order + printPoint = poly2polyEmit(polys, printPoint, function(poly, index, count) { + poly.forEachPoint(function(point, pidx, points, offset) { + // scale speed of first cutting poly since it engages the full bit + camOut(point.clone(), offset !== 0, undefined, count === 1 ? engageFactor : 1); + }, poly.isClosed(), index); + }, { swapdir: false }); + newLayer(); + } + progress(++total, sliceOut.length); + } + + // crucially returns true for -0 as well as other negative #s + function isNeg(v) { + return v < 0 || (v === 0 && 1/v === -Infinity); + } + + if (depthFirst) { + // get inside vals (the positive ones) + let ins = depthData.map(a => a.filter(p => !isNeg(p.depth))); + let itops = ins.map(level => { + return POLY.nest(level.filter(poly => poly.depth === 0).clone()); + }); + // get outside vals (the negative ones) + let outs = depthData.map(a => a.filter(p => isNeg(p.depth))); + let otops = outs.map(level => { + return POLY.nest(level.filter(poly => poly.depth === 0).clone()); + }); + printPoint = depthRoughPath(printPoint, 0, ins, itops, polyEmit, false, easeDown); + printPoint = depthRoughPath(printPoint, 0, outs, otops, polyEmit, false, easeDown); + } + } + /** + * Ease down along the polygonal path. + * + * 1. Travel from fromPoint to closest point on polygon, to rampZ above that that point, + * 2. ease-down starts, following the polygonal path, decreasing Z at a fixed slope until target Z is hit, + */ + function generateEaseDown(fn,poly, fromPoint, degrees = 45){ + let index = poly.findClosestPointTo(fromPoint).index, + fromZ = fromPoint.z, + offset = 0, + points = poly.points, + length = points.length, + touch = -1, // first point to touch target z + targetZ = points[0].z, + dist2next, + last, + next, + done; + + // Slope for computations. + const slope = Math.tan((degrees * Math.PI) / 180); + // Z height above polygon Z from which to start the ease-down. + // Machine will travel from "fromPoint" to "nearest point x, y, z' => with z' = point z + rampZ", + // then start the ease down along path. + const rampZ = 2.0; + while (true) { + next = points[index % length]; + if (last && next.z < fromZ) { + // When "in Ease-Down" (ie. while target Z not yet reached) - follow path while slowly decreasing Z. + let deltaZ = fromZ - next.z; + dist2next = last.distTo2D(next); + let deltaZFullMove = dist2next * slope; + + if (deltaZFullMove > deltaZ) { + // Too long: easing along full path would overshoot depth, synth intermediate point at target Z. + // + // XXX: please check my super basic trig - this should follow from `last` to `next` up until the + // intersect at the target Z distance. + fn(last.followTo(next, dist2next * deltaZ / deltaZFullMove).setZ(next.z), offset++); + } else { + // Ok: execute full move at desired slope. + next = next.clone().setZ(fromZ - deltaZFullMove); + } + + fromZ = next.z; + } else if (offset === 0 && next.z < fromZ) { + // First point, move to rampZ height above next. + let deltaZ = fromZ - next.z; + fromZ = next.z + Math.min(deltaZ, rampZ) + next = next.clone().setZ(fromZ); + } + last = next; + fn(next, offset++); + if (touch < 0 && next.z <= targetZ) { + // Save touch-down index so as to be able to "complete" the full cut at target Z, + // i.e. keep following the path loop until the touch down point is reached again. + touch = ((index + length) % length); + break; //break after touch + } + index++; + } + + return touch; + } + + // coming from a previous widget, use previous last point + lastPoint = firstPoint; + + // make top start offset configurable + printPoint = firstPoint || origin; + + // accumulated data for depth-first optimizations + // let depthData = { + // rough: [], + // outline: [], + // roughDiam: 0, + // outlineDiam: 0, + // contourx: [], + // contoury: [], + // trace: [], + // drill: [], + // layer: 0, + // }; + + let ops = { + stock, + setTool, + setDrill, + setSpindle, + setTolerance, + setPrintPoint, + setLasering, + getPrintPoint() { return printPoint }, + printPoint, + newLayer, + addGCode, + camOut, + polyEmit, + poly2polyEmit, + tip2tipEmit, + depthRoughPath, + depthOutlinePath, + sliceOutput, + emitDrills, + emitTrace, + bounds, + zclear, + zmax, + lastPoint: () => { return lastPoint } + }; + + let opSum = 0; + let opTot = widget.camops.map(op => op.weight()).reduce((a,v) => a + v); + + for (let op of widget.camops) { + setTolerance(0); + nextIsMove = true; + currentOp = op.op; + isIndex = currentOp.type === 'index'; + isLathe = currentOp.type === 'lathe'; + isRough = currentOp.type === 'rough'; + isPocket = currentOp.type === 'pocket'; + isContour = currentOp.type === 'contour' || (isPocket && currentOp.contour); + let weight = op.weight(); + newLayer(op.op); + op.prepare(ops, (progress, message) => { + update((opSum + (progress * weight)) / opTot, message || op.type(), message); + }); + opSum += weight; + if (tool && lastPoint) { + newLayer(); + if (!isIndex) { + layerPush(printPoint = lastPoint.clone().setZ(zmax_outer), 0, 0, tool); + newLayer(); + } + } + } + + function emitTrace(slice) { + let { tool, rate, plunge } = slice.camTrace; + setTool(tool, rate, plunge); + let traceTool = new Tool(settings, tool); + let traceToolDiam = traceTool.fluteDiameter(); + printPoint = poly2polyEmit(slice.camLines, printPoint, polyEmit, { swapdir: false }); + newLayer(); + } + + /** + * Output a single polygon as gcode. The polygon is walked in either the + * clockwise or counter-clockwise direction depending on the winding of the + * polygon. The first point of the polygon is assumed to be the starting + * point, and the last point is assumed to be the ending point. If the + * polygon is closed, the starting and ending points are the same. The + * function will automatically output a rapid move to the first point of + * the polygon if that point is not the current position. + * + * @param {Polygon} poly - the polygon to output + * @param {number} index - unused + * @param {number} count - 1 to set engage factor + * @param {Point} fromPoint - the point to rapid move from + * @param {Object} camOutOpts - optional parameters to pass to camOut + * @returns {Point} - the last point of the polygon + */ + function polyEmit(poly, index, count, fromPoint) { + + let arcMax = Infinity, // no max arc radius + lineTolerance = 0.001; // do not consider points under 0.001mm for arcs + + fromPoint = fromPoint || printPoint; + let arcQ = []; + arcQ.angle = [] + + let closest = poly.findClosestPointTo(fromPoint); + let lastPoint = closest.point; + let startIndex = closest.index; + + // console.log({poly, index, count, fromPoint,startIndex}) + + // scale speed of first cutting poly since it engages the full bit + let scale = ((isRough || isPocket) && count === 1) ? engageFactor : 1; + + + if (easeDown && poly.isClosed()) { //if doing ease-down + + let last = generateEaseDown((point,offset )=>{ //generate ease-down points + if(offset == 0) camOut(point.clone(), 0, {factor:engageFactor}); + camOut(point.clone(), 1, {factor:scale}); // and pass them to camOut + }, poly, fromPoint, easeAngle); + lastPoint = poly.points[last]; + startIndex = last; + } + + // console.log(poly,poly.isClosed(),startIndex) + //A is first point of segment, B is last + poly.forEachSegment( ( pointA, pointB, indexA, indexB) => { + // if(offset == 0) console.log("forEachPoint",point,pidx,points) + // console.log({pointA, pointB, indexA, indexB,startIndex}) + if(indexA == startIndex){ + camOut(pointA.clone(), 0, {factor:engageFactor}); + // if first point, move to and call export function + quePush(pointA); + } + lastPoint = arcExport(pointB, pointA); + }, !poly.isClosed(), startIndex); + + // console.log("at end of arcExport",structuredClone(arcQ)); + if(arcQ.length > 3){ + //if few points left, emit as lines + drainQ(); + } + while (arcQ.length){ + camOut(arcQ.shift(),1); + } + + function quePush(point){ + arcQ.push(point); + } + + function arcExport(point,lastp){ + let dist = lastp? point.distTo2D(lastp) : 0; + if (lastp) { + if (dist >lineTolerance && lastp) { + let rec = Object.assign(point,{dist}); + arcQ.push(rec); + + // ondebug({arcQ}); + if (arcQ.length > 2) { + let el = arcQ.length; + let e1 = arcQ[0]; // first in arcQ + let e2 = arcQ[Math.floor(el/2)]; // mid in arcQ + let e3 = arcQ[el-1]; // last in arcQ + let e4 = arcQ[el-2]; // second last in arcQ + let e5 = arcQ[el-3]; // third last in arcQ + let cc = util.center2d(e1, e2, e3, 1); // find center + let lr = util.center2d(e3, e4, e5, 1); // find local radius + let dc = 0; + + let radFault = false; + if (lr) { + let angle = 2 * Math.asin(dist/(2*lr.r)); + radFault = Math.abs(angle) > arcRes; // enforce arcRes(olution) + + } else { + radFault = true; + // console.log("too much angle") + } + + if (cc) { + if ([cc.x,cc.y,cc.z,cc.r].hasNaN()) { + // console.log({cc, e1, e2, e3}); + } + if (arcQ.length === 3) { + arcQ.center = [ cc ]; + arcQ.xSum = cc.x; + arcQ.ySum = cc.y; + arcQ.rSum = cc.r; + + // check if first angles should have caused radFault + let a = toRadians(arcQ[0].slopeTo(cc).angle) + let b = toRadians(arcQ[1].slopeTo(cc).angle) + let angle = Math.abs(b-a) + + if( Math.abs(angle) > arcRes){ + // if so, remove first point + camOut(arcQ.shift(), 1) + radFault = true + } + } else { + // check center point delta + arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); + arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); + arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); + let dx = cc.x - arcQ.xSum / arcQ.center.length; + let dy = cc.y - arcQ.ySum / arcQ.center.length; + dc = Math.hypot(dx, dy); // delta center distance + } + // if new point is off the arc + // if (deem || depm || desp || dc > arcTolerance || cc.r < arcMin || cc.r > arcMax || dist > cc.r) { + if ( dc * arcQ.center.length / arcQ.rSum > arcTolerance || dist > cc.r || cc.r > arcMax || radFault ) { + // let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcTolerance, dist > cc.r, cc.r > arcMax, radFault]; + // console.log("point off the arc,",structuredClone(arcQ),); + if (arcQ.length === 4) { + // not enough points for an arc, drop first point and recalc center + camOut(arcQ.shift(),1); + let tc = util.center2d(arcQ[0], arcQ[1], arcQ[2], 1); + // the new center is invalid as well. drop the first point + if (!tc) { + camOut(arcQ.shift(),1); + } else { + arcQ.center = [ tc ]; + let angle = 2 * Math.asin(arcQ[1].dist/(2*tc.r)); + if (Math.abs(angle) > arcRes) { // enforce arcRes on initial angle + camOut(arcQ.shift(),1); + } + } + } else { + // enough to consider an arc, emit and start new arc + let defer = arcQ.pop(); + drainQ(); + // re-add point that was off the last arc + arcQ.push(defer); + } + } else { + // new point is on the arc + arcQ.center.push(cc); + } + } else { + // drainQ on invalid center + drainQ(); + } + } + } else { + // if dist to small, output as a cut + // console.trace('point too small', point,lastp,dist); + camOut(point, 1); + } + } else { + // if first point, emit and set + + camOut(point, 1); + // TODO disabling out of plane z moves until a better mechanism + // can be built that doesn't rely on computed zpos from layer heights... + // when making z moves (like polishing) allow slowdown vs fast seek + // let moveSpeed = (lastp && lastp.z !== z) ? speedMMM : seekMMM; + // moveTo({x:x, y:y, z:z}, moveSpeed); + } + return point; + } + + function drainQ() { + + let arcPreviewRes = 64 + + if (!arcTolerance) { + return; + } + if (arcQ.length > 4) { + // ondebug({arcQ}); + let vec1 = new THREE.Vector2(arcQ[1].x - arcQ[0].x, arcQ[1].y - arcQ[0].y); + let vec2 = new THREE.Vector2(arcQ.center[0].x - arcQ[0].x, arcQ.center[0].y - arcQ[0].y); + let clockwise = vec1.cross(vec2) < 0 + let gc = clockwise ? 2 :3 + let from = arcQ[0]; + let to = arcQ.peek(); + let delta = from.distTo2D(to) + arcQ.xSum = arcQ.center.reduce( (t, v) => t + v.x , 0 ); + arcQ.ySum = arcQ.center.reduce( (t, v) => t + v.y , 0 ); + arcQ.rSum = arcQ.center.reduce( (t, v) => t + v.r , 0 ); + let cl = arcQ.center.length; + let center = newPoint( + arcQ.xSum / cl, + arcQ.ySum / cl, + ) + + // console.log("draining") + + if(arcQ.length == poly.points.length ){ + //if is a circle + // generate circle + // console.log("circle",{from, to,center}); + let arcPoints = arcToPath( from, from, arcPreviewRes,{ clockwise,center}) + .map(applyWidgetMovement); + camOut(from,1); + camOut(from,gc,{ center:center.sub(from), clockwise, arcPoints}); + // lastPoint = to.clone(); + }else{ + //if a non-circle arc + let arcPoints = arcToPath( from, to, arcPreviewRes,{ clockwise,center}) + .map(applyWidgetMovement); + // console.log("arc") + // first arc point + camOut(from,1); + // rest of arc to final point + camOut(to,gc,{ center:center.sub(from), clockwise, arcPoints}); + lastPoint = to.clone(); + } + + } else { + //if q too short, emit as lines + for (let rec of arcQ) { + camOut(rec,1); + } + lastPoint = arcQ.peek().clone(); + } + arcQ.length = 0; + arcQ.center = undefined; + } + + // newLayer(); + return lastPoint; + } + + function depthRoughPath(start, depth, levels, tops, emitter, fit, ease) { + let level = levels[depth]; + if (!(level && level.length)) { + return start; + } + let ltops = tops[depth]; + let fitted = fit ? ltops.filter(poly => poly.isInside(fit, 0.05)) : ltops; + let ftops = fitted.filter(top => !top.level_emit); + if (ftops.length > 1) { + ftops = POLY.route(ftops, start); + } + + function roughTopEmit(top, index, count, start) { + top.level_emit = true; + let inside = level.filter(poly => poly.isInside(top)); + if (ease) { + start.z += ease; + } + start = poly2polyEmit(inside, start, emitter, { mark: "emark", perm: true, swapdir: false }); + if (ease) { + start.z += ease; + } + start = depthRoughPath(start, depth + 1, levels, tops, emitter, top, ease); + return start; + } + + // output fragments (due to tabs) last + let frag = ftops.filter(p => p.open); + let full = ftops.filter(p => !p.open); + + poly2polyEmit(full, start, roughTopEmit, { mark: "emark", swapdir: false }); + poly2polyEmit(frag, start, roughTopEmit, { mark: "emark", swapdir: false }); + + return start; + } + + function depthOutlinePath(start, depth, levels, radius, emitter, dir, ease) { + let bottm = depth < levels.length - 1 ? levels[levels.length - 1] : null; + let above = levels[depth-1]; + let level = levels[depth]; + if (!level) { + return start; + } + if (above) { + level = level.filter(lp => { + const conf = above.filter(ap => !ap.level_emit && lp.isNear(ap, radius, true)); + return conf.length === 0; + }); + } + // const thru = []; // match thru polys + level = level.filter(lp => { + if (lp.level_emit) { + return false; + } + // if (bottm && !clr) { + // const tm = bottm.filter(bp => lp.isEquivalent(bp)); + // thru.appendAll(tm); + // return tm.length === 0; + // } + return true; + }); + // limit level search to polys matching winding (inside vs outside) + level = level.filter(p => p.isClockwise() === dir); + // omit polys that match bottom level polys unless level above is cleared + start = poly2polyEmit(level, start, (poly, index, count, fromPoint) => { + poly.level_emit = true; + if (ease) { + fromPoint.z += ease; + } + fromPoint = polyEmit(poly, index, count, fromPoint); + if (ease) { + fromPoint.z += ease; + } + fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, dir, ease); + fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, !dir, ease); + return fromPoint; + }, { weight: false, swapdir: false }); + return start; + } + + // last layer/move is to zmax + // re-inject that point into the last layer generated + if (lastPoint && newOutput.length) { + let lastLayer = newOutput.filter(layer => Array.isArray(layer)).peek(); + if (Array.isArray(lastLayer)) { + print.addOutput(lastLayer, printPoint = lastPoint.clone().setZ(zmax_outer), 0, 0, tool); + } + } + // console.log("prepare output", newOutput); + // replace output single flattened layer with all points + print.output = newOutput; + return printPoint; +}; + +/** + * return tool Z clearance height for a line segment movement path + */ +function getZClearPath(terrain, x1, y1, x2, y2, z, zadd, off, over) { + // when terrain skipped, top + pass used + if (terrain > 0) { + return terrain; + } + let maxz = z; + let check = []; + for (let i=0; i p.intersections(p1, p2, true)).flat(); + if (int.length) { + maxz = Math.max(maxz, data.z + zadd + over); + continue; + } + let s1 = p1.slopeTo(p2).toUnit().normal(); + let s2 = p2.slopeTo(p1).toUnit().normal(); + let pa = p1.projectOnSlope(s1, off); + let pb = p2.projectOnSlope(s1, off); + int = data.tops.map(p => p.intersections(pa, pb, true)).flat(); + if (int.length) { + maxz = Math.max(maxz, data.z + zadd + over); + continue; + } + pa = p1.projectOnSlope(s2, off); + pb = p2.projectOnSlope(s2, off); + int = data.tops.map(p => p.intersections(pa, pb, true)).flat(); + if (int.length) { + maxz = Math.max(maxz, data.z + zadd + over); + continue; + } + } + return maxz; +} diff --git a/src2/kiri-mode/cam/slice.js b/src2/kiri-mode/cam/slice.js new file mode 100644 index 00000000..63831e57 --- /dev/null +++ b/src2/kiri-mode/cam/slice.js @@ -0,0 +1,739 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { base, util } from '../../geo/base.js'; +import { newLine } from '../../geo/line.js'; +import { newPoint } from '../../geo/point.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { setSliceTracker } from '../../kiri/slice.js'; +import { ops as OPS } from './ops.js'; +import { Tool } from './tool.js'; +import { Slicer as cam_slicer } from './slicer.js'; + +/** + * DRIVER SLICE CONTRACT + * + * @param {Object} settings + * @param {Widget} widget + * @param {Function} output + */ +export async function cam_slice(settings, widget, onupdate, ondone) { + let proc = settings.process, + sliceAll = widget.slices = [], + camOps = widget.camops = [], + isIndexed = proc.camStockIndexed; + + let stock, bounds, track, + camZTop, camZBottom, camZThru, wztop, ztOff, zbOff, + zBottom, zMin, zMax, zThru, zTop, + minToolDiam, maxToolDiam, dark, color, tabs, unsafe, units, + axisRotation, axisIndex, + part_size, + bottom_gap, bottom_part, bottom_stock, bottom_thru, bottom_z, bottom_cut, + top_stock, top_part, top_gap, top_z, + workarea; + + console.log('CAM_SLICE', [...arguments]); + + // allow recomputing later if widget or settings changes + const var_compute = () => { + let { camStockX, camStockY, camStockZ, camStockOffset } = proc; + bounds = widget.getBoundingBox(); + stock = camStockOffset ? { + x: bounds.dim.x + camStockX, + y: bounds.dim.y + camStockY, + z: bounds.dim.z + camStockZ, + } : { + x: camStockX, + y: camStockY, + z: camStockZ + } + track = widget.track; + ({ camZTop, camZBottom, camZThru } = proc); + wztop = track.top; + ztOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (stock.z - wztop); + zbOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (wztop - track.box.d); + zBottom = isIndexed ? camZBottom : camZBottom - zbOff; + zMin = isIndexed ? bounds.min.z : Math.max(bounds.min.z, zBottom); + zMax = bounds.max.z; + zThru = camZThru; + zTop = zMax + ztOff; + minToolDiam = Infinity; + maxToolDiam = -Infinity; + dark = !!settings.controller.dark; + color = dark ? 0xbbbbbb : 0; + tabs = widget.anno.tab; + unsafe = proc.camExpertFast; + units = settings.controller.units === 'in' ? 25.4 : 1; + axisRotation = axisIndex = undefined; + part_size = bounds.dim; + bottom_gap = zbOff; + bottom_part = 0; + bottom_stock = -bottom_gap; + bottom_thru = zThru; + bottom_z = isIndexed ? zBottom : Math.max( + (camZBottom ? bottom_stock + camZBottom : bottom_part) - bottom_thru, + (camZBottom ? bottom_stock + camZBottom : bottom_stock - bottom_thru) + ); + bottom_cut = Math.max(bottom_z, -zThru); + top_stock = zTop; + top_part = zMax; + top_gap = ztOff; + top_z = camZTop ? bottom_stock + camZTop : top_stock; + workarea = util.round({ + top_stock, top_part, top_gap, top_z, + bottom_stock, bottom_part, bottom_gap, + bottom_z, bottom_cut + }, 3); + + return structuredClone(workarea); + }; + + // initial setup + var_compute(); + + if (tabs) { + // make tab polygons + tabs.forEach(tab => { + let zero = newPoint(0,0,0), + point = newPoint(tab.pos.x, tab.pos.y, tab.pos.z), + poly = newPolygon().centerRectangle(zero, tab.dim.x, tab.dim.y), + [ rx, ry, rz, rw ] = tab.rot, + m4 = new THREE.Matrix4().makeRotationFromQuaternion( + new THREE.Quaternion(rx, ry, rz, rw) + ); + poly.points = poly.points + .map(p => new THREE.Vector3(p.x,p.y,p.z).applyMatrix4(m4)) + .map(v => newPoint(v.x, v.y, v.z)); + poly.move(point); + tab.poly = poly; + // tslice.output().setLayer("tabs", 0xff0000).addPoly(poly); + // sliceAll.push(tslice); + }); + } + + function error(msg) { + ondone(msg); + } + + if (unsafe) { + console.log("disabling overhang safeties"); + } + + if (!proc.ops || proc.ops.length === 0) { + return error('no processes specified'); + } + + if (stock.x === 0 || stock.y === 0 || stock.z === 0) { + return error("one or more stock dimensions is zero
offset stock or set to non zero value"); + } + + if (stock.x && stock.y && stock.z && !isIndexed) { + if (stock.x + 0.00001 < bounds.max.x - bounds.min.x) { + return error('stock X too small for part. resize stock or use offset stock'); + } + + if (stock.y + 0.00001 < bounds.max.y - bounds.min.y) { + return error('stock Y too small for part. resize stock or use offset stock'); + } + + if (stock.z + 0.00001 < bounds.max.z - bounds.min.z) { + return error('stock Z too small for part. resize stock or use offset stock'); + } + } + + if (zMin >= bounds.max.z) { + return error(`invalid z bottom ${(zMin/units).round(3)} >= bounds z max ${(zMax/units).round(3)}`); + } + + let mark = Date.now(); + let opList = []; + let opSum = 0; + let opTot = 0; + let shadows = {}; + let slicer; + let state = { + settings, + widget, + bounds, + tabs, + cutTabs, + cutPolys, + contourPolys, + healPolys, + shadowAt, + slicer, + addSlices, + isIndexed, + setAxisIndex, + updateToolDiams, + updateSlicer, + computeShadows, + zBottom, + zThru, + ztOff, + zMax, + zTop, + unsafe, + color, + dark, + ops: opList + }; + let tracker = setSliceTracker({ rotation: 0 }); + + function updateSlicer() { + slicer = state.slicer = new cam_slicer(widget); + } + + async function computeShadows() { + shadows = {}; + await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => { + // console.log('reshadow', progress.round(3)); + }); + } + + function updateToolDiams(toolDiam) { + minToolDiam = Math.min(minToolDiam, toolDiam); + maxToolDiam = Math.max(maxToolDiam, toolDiam); + } + + function shadowAt(z) { + let cached = shadows[z]; + if (cached) { + return cached; + } + // find closest shadow above and use to speed up delta shadow gen + let minZabove; + let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z); + for (let zkey of zover) { + if (minZabove && zkey < minZabove) { + minZabove = zkey; + } else { + minZabove = zkey; + } + } + let shadow = shadowAt(widget, z, minZabove); + if (minZabove) { + // const merge = shadow.length; + // const plus = shadows[minZabove].length; + // const mark = Date.now(); + shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true); + // console.log({merge, plus, equals: shadow.length, time: Date.now() - mark}); + } + return shadows[z] = POLY.setZ(shadow, z); + } + + function setAxisIndex(degrees = 0, absolute = true) { + axisIndex = absolute ? degrees : axisIndex + degrees; + axisRotation = (Math.PI / 180) * axisIndex; + widget.setAxisIndex(isIndexed ? -axisIndex : 0); + return isIndexed ? -axisIndex : 0; + } + + function addPolyIndexing(poly, a) { + if (!poly) { + return; + } + if (Array.isArray(poly)) { + for (let p of poly) { + addPolyIndexing(p, a); + } + return; + } + for (let point of poly.points) { + point.a = a; + } + addPolyIndexing(poly.inner, a); + } + + function addSlices(slices, addIndexing = isIndexed) { + if (!Array.isArray(slices)) { + slices = [ slices ]; + } + sliceAll.appendAll(slices); + if (addIndexing && axisIndex !== undefined) { + // update slice cam lines to add axis indexing + for (let slice of slices.filter(s => s.camLines)) { + addPolyIndexing(slice.camLines, -axisIndex); + } + } + } + + if (false) { + opList.push(new OPS.xray(state, { type: "xray" })); + } + + let activeOps = proc.ops.filter(op => !op.disabled); + + // silently preface op list with OpShadow + if (isIndexed) { + if (activeOps.length === 0 || activeOps[0].type !== 'index') + opList.push(new OPS.index(state, { type: "index", index: 0 })); + } else { + opList.push(new OPS.shadow(state, { type: "shadow", silent: true })); + } + + // determing # of steps and step weighting for progress bar + for (let op of activeOps) { + if (op.type === '|') { + break; + } + let opfn = OPS[op.type]; + if (opfn) { + let opin = new opfn(state, op); + opList.push(opin); + opTot += opin.weight(); + } + } + + // call slice() function on all ops in order + setAxisIndex(); + updateSlicer(); + for (let op of opList) { + let weight = op.weight(); + // apply operation override vars + let workover = var_compute(); + let valz = op.op; + if (valz.ov_topz) { + workover.top_z = isIndexed ? valz.ov_topz : bottom_stock + valz.ov_topz; + } + if (valz.ov_botz) { + workover.bottom_z = isIndexed ? valz.ov_botz : bottom_stock + valz.ov_botz; + workover.bottom_cut = Math.max(workover.bottom_z, -zThru); + } + // state.workarea = workover; + Object.assign(state, { + zBottom, + zThru, + ztOff, + zMax, + zTop, + workarea: workover + }); + // console.log({ + // op, + // workover, + // bounds: structuredClone(bounds), + // stock: structuredClone(stock) + // }); + await op.slice((progress, message) => { + onupdate((opSum + (progress * weight)) / opTot, message || op.type()); + }); + // update tracker rotation for next slice output() visualization + tracker.rotation = isIndexed ? axisRotation : 0; + camOps.push(op); + opSum += weight; + } + setSliceTracker(); + + // reindex + sliceAll.forEach((slice, index) => slice.index = index); + + // used in printSetup() + // used in CAM.prepare.getZClearPath() + // add tabs to terrain tops so moves avoid them + if (tabs) { + state.terrain.forEach(slab => { + tabs.forEach(tab => { + if (tab.pos.z + tab.dim.z/2 >= slab.z) { + let all = [...slab.tops, tab.poly]; + slab.tops = POLY.union(all, 0, true); + // slab.slice.output() + // .setLayer("debug-tabs", {line: 0x880088, thin: true }) + // .addPolys(POLY.setZ(slab.tops.clone(true), slab.z), { thin: true }); + } + }); + }); + } + + // add shadow perimeter to terrain to catch outside moves off part + let tabpoly = tabs ? tabs.map(tab => tab.poly) : []; + let allpoly = POLY.union([...state.shadow.base, ...tabpoly], 0, true); + let shadowOff = maxToolDiam < 0 ? allpoly : + POLY.offset(allpoly, [minToolDiam/2,maxToolDiam/2], { count: 2, flat: true, minArea: 0 }); + state.terrain.forEach(level => level.tops.appendAll(shadowOff)); + + widget.terrain = state.skipTerrain ? null : state.terrain; + widget.minToolDiam = minToolDiam; + widget.maxToolDiam = maxToolDiam; + + ondone(); +}; + +export function addDogbones(poly, dist, reverse) { + if (Array.isArray(poly)) { + return poly.forEach(p => addDogbones(p, dist)); + } + let open = poly.open; + let isCW = poly.isClockwise(); + if (reverse || poly.parent) isCW = !isCW; + let oldpts = poly.points.slice(); + let lastpt = oldpts[oldpts.length - 1]; + let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); + let length = oldpts.length + (open ? 0 : 1); + let newpts = [ ]; + for (let i=0; i 1 && i < length)) { + if (isCW && adiff > 45) { + let newa = base.newSlopeFromAngle(lastsl.angle + bdiff); + newpts.push(lastpt.projectOnSlope(newa, dist)); + newpts.push(lastpt.clone()); + } else if (!isCW && adiff < -45) { + let newa = base.newSlopeFromAngle(lastsl.angle - bdiff); + newpts.push(lastpt.projectOnSlope(newa, dist)); + newpts.push(lastpt.clone()); + } + } + lastsl = nextsl; + lastpt = nextpt; + if (i < oldpts.length) { + newpts.push(nextpt); + } + } + poly.points = newpts; + if (poly.inner) { + addDogbones(poly.inner, dist, true); + } +}; + +export async function traces(settings, widget, single) { + if (widget.traces) { + return false; + } + + // --- points → line segments --- + let edges = new THREE.EdgesGeometry(widget.mesh.geometry, settings.controller.edgeangle ?? 20); + let array = edges.attributes.position.array; + let pcache = {}; + let points = new Array(2); + let lines = new Array(points.length / 2); + for (let i=0, j=0, k=0, l=array.length; i (v * 10000) | 0).join(','); + let point = pcache[key]; + if (!point) { + point = newPoint(ps[0], ps[1], ps[2], key); + point.lines = []; + pcache[key] = point; + } + points[j++ % 2] = point; + if (j % 2 === 0) { + let [ p0, p1 ] = points; + let line = lines[k++] = newLine(p0, p1); + p0.lines.push(line); + p1.lines.push(line); + } + } + + // --- segments → chains (bidirectional walk) --- + let chains = []; + { + const segs = lines.slice(); // shallow copy + segs.forEach(s => (s.visited = false)); + + function step(dirPoint, prevSeg, pushFront, chain) { + let curr = dirPoint; + let prev = prevSeg; + + while (curr.lines.length === 2) { // stay inside the arc + const next = curr.lines.find(l => !l.visited && l !== prev); + if (!next) break; // should not happen + + next.visited = true; + if (pushFront) chain.unshift(next); // prepend or append? + else chain.push(next); + + curr = next.p1 === curr ? next.p2 : next.p1; + prev = next; + } + } + + for (const seed of segs) { + if (seed.visited) continue; + + seed.visited = true; // always include the seed + const chain = [seed]; + + // grow backwards from p1 (prepend) + step(seed.p1, seed, true, chain); + + // grow forwards from p2 (append) + step(seed.p2, seed, false, chain); + + chains.push(chain); + } + } + + // --- chains → polylines --- + const polylines = []; + + for (const chain of chains) { + const poly = newPolygon().setOpen(); + + // choose any node whose degree !== 2; if none, it’s a closed loop + let start = null; + for (const s of chain) { + if (s.p1.lines.length !== 2) { start = s.p1; break; } + if (s.p2.lines.length !== 2) { start = s.p2; break; } + } + if (!start) start = chain[0].p1; // closed loop + + let curr = start, prevSeg = null; + poly.push(curr); // first point + + while (true) { + const nextSeg = curr.lines.find( + l => l !== prevSeg && chain.includes(l) + ); + if (!nextSeg) break; // open end reached + + curr = nextSeg.p1 === curr ? nextSeg.p2 : nextSeg.p1; + poly.push(curr); // << push *before* test + if (curr === start) break; // loop closed + + prevSeg = nextSeg; + } + polylines.push(poly); + } + + widget.traces = polylines; + + return true; +}; + +/** + * Generate a list of holes in the model based on the given diameter. + * + * @param {Object} settings - settings object + * @param {Object} widget - widget object + * @param {boolean} individual - if true, drill holes individually + * @param {Object} rec - DrillOp record + * @param {Function} onProgress - callback function to report progress + * @returns {Array} list of hole centers as objects with `x`, `y`, `z`, `depth`, and `selected` properties. + */ +export async function holes(settings, widget, individual, rec,onProgress) { + + let {tool,mark,precision} = rec //TODO: display some visual difference if mark is selected + let toolDiam = new Tool(settings,tool).fluteDiameter() + let diam = individual ? 1 : toolDiam; // sets default diameter when select individual used + + let proc = settings.process, + stock = settings.stock || {}, + isIndexed = proc.camStockIndexed, + track = widget.track, + { camZTop, camZBottom, camZThru } = proc, + // widget top z as defined by setTopz() + wztop = track.top, + // distance between top of part and top of stock + ztOff = isIndexed ? 0 : (stock.z - wztop), + // distance between bottom of part and bottom of stock + zbOff = isIndexed ? 0 : (wztop - track.box.d), + // defined z bottom offset by distance to stock bottom + // keeps the z bottom relative to the part when z align changes + zBottom = isIndexed ? camZBottom : camZBottom - zbOff; + + + let slicerOpts = {flatoff: 0.001} + let slicer = new cam_slicer(widget,slicerOpts); + let zFlats = Object.keys(slicer.zFlat).map(Number).map(z=>[z,z-0.002]).flat() + + precision = Math.max( 0, precision ) + let intervals = (precision == 0) ? [] : slicer.interval( + precision, + { + fit: false, off: -0.01, flats: true + } + ) + + let zees = [...zFlats,...intervals] + let indices = [...new Set(zees + .map(kv => parseFloat(kv).round(5)) + .filter(z => z !== null) + )] + let centerDiff = diam * 0.1, + area = (diam/2) * (diam/2) * Math.PI, + circles = [], + slices = []; + + function onEach(slice) { + slices.push(slice); + } + let opts = { each: onEach, progress: (num,total)=> onProgress(num/total*0.5,"slice") }; + await slicer.slice(indices, opts); + let shadowedDrills = false; + // console.log("slices",slices) + for (let [i,slice] of slices.entries()) { + for(let top of slice.tops){ + // console.log("slicing",slice.z,top) + slice.shadow = shadowAt(widget, slice.z, 0) + let inner = top.inner; + if (!inner) { //no holes + continue; + } + for (let poly of inner) { + if ( poly.points.length < 7 ) continue; + let center = poly.calcCircleCenter(); + center.area = poly.area(); + center.overlapping = [center] + center.depth = 0; + // console.log("center",center) + if ( poly.circularity() < 0.985 ){ + // if not circular, don't add to holes + continue; + } + if (center.isInPolygon(slice.shadow)) { + // if shadowed, don't add, and inform client + shadowedDrills = true; + continue; + } + let overlap = false; + for (let [i,circle] of circles.entries()) { + let dist = circle.distTo2D(center) + + // //if on the same xy point, + if (dist <= centerDiff ) { + // console.log("overlap",center,circle); + circle.overlapping.push(center); + // if overlapping, don't add and continue + overlap = true; + continue; + } + } + if (!overlap) circles.push(center); + } + } + onProgress(0.5+(i/slices.length*0.25),"recognize circles") + } + + let drills = [] + + for (let [i,c] of circles.entries()) { + let overlapping = c.overlapping + + let last = overlapping.shift(); + while (overlapping.length) { + let circ = overlapping.shift(); + let aveArea = (circ.area + last.area) / 2; + let areaDelta = Math.abs(circ.area -last.area) + if (areaDelta < aveArea * 0.05){ // if area delta less than 5% of average area + //keep top circle selected + last.depth = last.z - circ.z; + }else{ // if not the same area + //push and move on + drills.push(last); + last = circ; + } + } + if (last.depth != 0) drills.push(last) //add last circle + onProgress(0.75+(i/circles.length*0.25),"assemble holes") + } + drills.forEach( h=>{ + if(rec.fromTop){ + // set z top if selected + h.depth += wztop - h.z + h.z = wztop + } + delete h.overlapping //for encoding + h.diam = toolDiam // for mesh generation + h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05 ); //for same size selection + }) + + // console.log("unfiltered circles",circles) + // console.log("drills",drills) + drills = drills.filter(drill => drill.depth > 0) + widget.shadowedDrills = shadowedDrills + widget.drills = drills; + return drills; +} + +function cutTabs(tabs, offset, z, inter) { + tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z/2).map(tab => tab.off).flat(); + return cutPolys(tabs, offset, z, false); +} + +function cutPolys(polys, offset, z, inter) { + let noff = []; + offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), inter) )); + return healPolys(noff); +} + +function contourPolys(widget, polys) { + const raycaster = new THREE.Raycaster(); + raycaster.ray.direction.set(0, 0, -1); // ray pointing down Z + for (let poly of polys) { + for (let point of poly.points) { + raycaster.ray.origin.set(point.x, point.y, 10000); + const intersects = raycaster.intersectObject(widget.mesh, false); + const firstHit = intersects[0] || null; + point.z = firstHit.point.z; + } + } +} + +function healPolys(noff) { + for (let p of noff) { + if (p.appearsClosed()) { + p.points.pop(); + p.setClosed(); + } + } + if (noff.length > 1) { + let heal = 0; + // heal/rejoin open segments that share endpoints + outer: for(;; heal++) { + let ntmp = noff, tlen = ntmp.length; + for (let i=0; i 0.01) { + continue; + } + if (!(s1.open && s2.open)) continue; + if (s1.last().isMergable2D(s2.first())) { + s1.addPoints(s2.points.slice(1)); + ntmp[j] = null; + continue outer; + } + if (s2.last().isMergable2D(s1.first())) { + s2.addPoints(s1.points.slice(1)); + ntmp[i] = null; + continue outer; + } + if (s1.first().isMergable2D(s2.first())) { + s1.reverse(); + s1.addPoints(s2.points.slice(1)); + ntmp[j] = null; + continue outer; + } + if (s1.last().isMergable2D(s2.last())) { + s2.reverse(); + s1.addPoints(s2.points.slice(1)); + ntmp[j] = null; + continue outer; + } + } + } + break; + } + if (heal > 0) { + // cull nulls + noff = noff.filter(o => o); + } + // close poly if head meets tail + for (let poly of noff) { + if (poly.open && poly.first().isMergable2D(poly.last())) { + poly.points.pop(); + poly.open = false; + } + } + } + return noff; +} diff --git a/src2/kiri-mode/cam/slicer.js b/src2/kiri-mode/cam/slicer.js new file mode 100644 index 00000000..ffe8e18d --- /dev/null +++ b/src2/kiri-mode/cam/slicer.js @@ -0,0 +1,510 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { base, util } from '../../geo/base.js'; +import { broker } from '../../moto/broker.js'; +import { decode, decodePointArray, encode, encodePointArray } from '../../kiri/codec.js'; +import { newLine, newOrderedLine } from '../../geo/line.js'; +import { newPoint } from '../../geo/point.js'; +import { newSlice } from '../../kiri/slice.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { slicer } from '../../geo/slicer.js'; + +const { sliceConnect, sliceDedup } = slicer; +const { config } = base; +const timing = false; + +const begin = function() { + if (timing) console.time(...arguments); +}; + +const end = function() { + if (timing) console.timeEnd(...arguments); +}; + +export class Slicer { + constructor(widget, options = { zlist: true, zline: true, lines: false }) { + this.minions = self.kiri_worker.minions; + this.options = {}; + this.setOptions(options); + + if (widget) { + this.setPoints(widget.getGeoVertices({ unroll: true, translate: true })); + this.computeFeatures(); + } + } + + get threaded() { + return this.options.threaded; + } + + // zList = generate list of z vertices + // zline = generate list of z vertices with coplanar lines + // trace = find z coplanar trace lines + // flatoff = amount to offset z when slicing on detected flats + // each = call for each slice generated from an interval + setOptions(options) { + Object.assign(this.options, options || {}); + return this.options; + } + + setPoints(points) { + this.bounds = null; + this.points = points; + this.zFlat = {}; // accumulated flat area at z height + this.zLine = {}; // count of z coplanar lines + this.zList = {}; // count of z values for auto slicing + return this; + } + + // gather z-index stats + // these are used for auto-slicing in laser + // and to flats detection in CAM mode + computeFeatures(options) { + begin('compute features'); + + const opt = this.setOptions(options); + const bounds = this.bounds = new THREE.Box3(); + const points = this.points; + const zFlat = this.zFlat; + const zLine = this.zLine; + const zList = this.zList; + + function countZ(z) { + z = z.round(5); + zList[z] = (zList[z] || 0) + 1; + } + + let p1 = newPoint(0,0,0), + p2 = newPoint(0,0,0), + p3 = newPoint(0,0,0); + + // for (let i = 0, il = points.length; i < il; i++) { + // points[i] = points[i].round(5); + // } + + for (let i = 0, il = points.length; i < il; ) { + p1.set(points[i++], points[i++], points[i++]); + p2.set(points[i++], points[i++], points[i++]); + p3.set(points[i++], points[i++], points[i++]); + // update bounds + bounds.expandByPoint(p1); + bounds.expandByPoint(p2); + bounds.expandByPoint(p3); + // count occurrences of z values for auto slicing + if (opt.zlist) { + countZ(p1.z); + countZ(p2.z); + countZ(p3.z); + } + // use co-flat and co-line detection to adjust slice Z + if (p1.z === p2.z && p2.z === p3.z) { + // detect zFlat faces to avoid slicing directly on them + let zkey = p1.z.toFixed(5), + area = Math.abs(util.area2(p1,p2,p3)) / 2; + if (!zFlat[zkey]) { + zFlat[zkey] = area; + } else { + zFlat[zkey] += area; + } + } else if (opt.zline) { + // detect zLine (curved region tops/bottoms) + // in cam used for ball and v mill tracing + if (p1.z === p2.z) { + let zkey = p1.z.toFixed(5); + let zval = zLine[zkey]; + zLine[zkey] = (zval || 0) + 1; + } + if (p2.z === p3.z) { + let zkey = p2.z.toFixed(5); + let zval = zLine[zkey]; + zLine[zkey] = (zval || 0) + 1; + } + if (p3.z === p1.z) { + let zkey = p3.z.toFixed(5); + let zval = zLine[zkey]; + zLine[zkey] = (zval || 0) + 1; + } + } + } + + end('compute features'); + // console.log({ bounds, zFlat, zLine, zList }); + + return this; + } + + /** + * Given an array of z-slice positions, generate a list of polygons + * representing the intersection of the mesh with the given z-slice + * positions. + * + * @param {number[]} zs - array of z-slice positions + * @param {Object} options - slicing parameters + * @param {number} [options.flatoff=0.01] - z offset when slicing on a flat surface + * @param {boolean} [options.progress=false] - callback with slice progress + * @param {function} [options.each] - callback with each slice result + * @returns {Promise} - list of slice objects with z and tops (polygons) + */ + async slice(zs, options) { + const opt = this.setOptions(options); + // slice through points at given Z and return polygons + + if (!Array.isArray(zs)) { + throw "slice zs parameter must be ab array"; + } + + // sort Z and offset when on a flat + const flatoff = util.numOrDefault(opt.flatoff, 0.01); + zs = zs.sort((a,b) => a-b).map(z => { + if (!flatoff) { + return z; + } + const znorm = z.toFixed(5); + return this.zFlat[znorm] ? z + flatoff : z; + }); + + // compute buckets from z slice list + const { points } = this; + const plen = points.length; + const zlen = zs.length; + const ppz = plen / zlen; + const count = 25;//Math.ceil(ppz / 10000); + const step = Math.ceil(zlen / count); + const buckets = []; + + // console.log({ points, zs, count, step }); + + for (let c = 0, b = 0; c < count; c++) { + if (b >= zlen) break; + buckets.push({ + zs: zs.slice(b, b + step), + index: [] + }); + b += step; + } + + let p1 = newPoint(0,0,0), + p2 = newPoint(0,0,0), + p3 = newPoint(0,0,0), + ep = 0.001; + + begin("create buckets"); + for (let i = 0, il = points.length; i < il; ) { + p1.set(points[i++], points[i++], points[i++]); + p2.set(points[i++], points[i++], points[i++]); + p3.set(points[i++], points[i++], points[i++]); + let zmin = Math.min(p1.z, p2.z, p3.z); + let zmax = Math.max(p1.z, p2.z, p3.z); + for (let bucket of buckets) { + const { zs, index } = bucket; + const min = Math.min(...zs); + const max = Math.max(...zs); + if (zmin < min && zmax < min) { + if (Math.abs(zmin - min) > ep && Math.abs(zmax - min) > ep) { + continue; + } + } + if (zmin > max && zmax > max) { + if (Math.abs(zmin - max) > ep && Math.abs(zmax - max) > ep) { + continue; + } + } + index.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z); + } + } + end("create buckets"); + + // console.log({ zs, zlen, count, step, buckets, flatoff }); + + begin("slicing"); + const { minions } = this; + const threaded = minions && minions.running; + const sliceFn = (threaded ? this.sliceBucketMinion : this.sliceBucket).bind(this); + const track = { count: 0, total: zs.length }; + + if (threaded) minions.broadcast("cam_slice_init", {}); + + const promises = [] + for (let bucket of buckets) { + promises.push(sliceFn(bucket, { ...opt, minZ: this.bounds.min.z }, slice => { + track.count++; + if (opt.progress) { + opt.progress(track.count, track.total); + } + // console.log({ oneach: slice, ...track }); + })); + } + const data = (await Promise.all(promises)).flat(); + + data.sort((a,b) => b.z - a.z).forEach((rec, i) => { + rec.tops = rec.polys; + rec.slice = newSlice(rec.z).addTops(rec.tops); + if (opt.each) { + opt.each(rec, i, data.length); + } + }); + + if (threaded) minions.broadcast("cam_slice_cleanup"); + end("slicing"); + + return data; + } + + async sliceBucketMinion(bucket, opt, oneach) { + const slices = (await new Promise(resolve => { + this.minions.queue({ + cmd: "cam_slice", + opt: Object.clone(opt), + bucket, + }, resolve); + })).slices; + for (let slice of slices) { + slice.polys = decode(slice.polys); + if (slice.lines) { + slice.lines = decodePointArray(slice.lines) + .group(2) + .map(arr => { + return newLine(arr[0], arr[1]) + }); + } + oneach(slice); + } + // console.log(bucket, slices); + return slices; + } + + async sliceBucket(bucket, opt, oneach) { + const { zs, index } = bucket; + const slices = []; + for (let z of zs) { + const slice = this.sliceZ(z, index, opt); + if (slice) { + slices.push(slice); + } + oneach(slice); + } + return slices; + } + + sliceZ(z, indices, opt) { + let links = opt.links !== false, + dedup = opt.dedup !== false, + edges = opt.edges || false, + over = opt.over || z <= opt.minZ, + debug = opt.debug, + phash = {}, + lines = []; + + // const { points } = this, + const points = indices, + p1 = newPoint(0,0,0), + p2 = newPoint(0,0,0), + p3 = newPoint(0,0,0); + + for (let index = 0; index < points.length; ) { + p1.set(points[index++], points[index++], points[index++]); + p2.set(points[index++], points[index++], points[index++]); + p3.set(points[index++], points[index++], points[index++]); + + let where = {under: [], over: [], on: []}; + checkOverUnderOn(p1, z, where); + checkOverUnderOn(p2, z, where); + checkOverUnderOn(p3, z, where); + if (where.under.length === 3 || where.over.length === 3) { + // does not intersect (all 3 above or below) + } else if (where.on.length === 2) { + // one side of triangle is on the Z plane and 3rd is below + // drop lines with 3rd above because that leads to ambiguities + // with complex nested polygons on flat surface + if ((over && where.over.length === 1) || (!over && where.under.length === 1)) { + lines.push(makeZLine(phash, where.on[0], where.on[1], false, true)); + } + } else if (where.on.length === 3) { + // triangle is coplanar with Z + // we drop these because this face is attached to 3 others + // that will satisfy the if above (line) with 2 points + } else if (where.under.length === 0 || where.over.length === 0) { + // does not intersect but one point is on the slice Z plane + } else if (!edges) { + // compute two point intersections and construct line + let line = intersectPoints(where.over, where.under, z); + if (line.length < 2 && where.on.length === 1) { + line.push(where.on[0].clone()); + } + if (line.length === 2) { + lines.push(makeZLine(phash, line[0], line[1])); + } else { + console.log({msg: "invalid ips", line: line, where: where}); + } + } + } + + if (dedup) { + // console.log({ z, dedup: lines, points }); + lines = sliceDedup(lines, debug); + } + + return lines.length ? { z, + lines: opt.lines !== false ? lines : undefined, + polys: links ? POLY.nest(sliceConnect(lines, opt, debug)) : undefined + } : null; + } + + interval(step, options) { + let opt = options || {}, + bounds = this.bounds, + boff = opt.boff || opt.off || 0, // bottom offset + toff = opt.toff || opt.off || 0, // top offset + zmin = opt.min !== undefined ? opt.min : bounds.min.z + boff, + zmax = (opt.max || bounds.max.z) - toff, + steps = (zmax - zmin) / step, + rem = steps % 1 != 0 ? 0 : 1, + count = Math.floor(steps) + rem, + array = []; + + if (opt.fit) { + count++; + step = (zmax - zmin) / count; + } + if (opt.down) { + for (let i=0; i { + z = parseFloat(z); + add.push(z + opt.off); + if (z > zmin) { + add.push(z - opt.off); + } + }); + // add over and under all flats by 'off' + array.appendAll(add).sort((a,b) => { + return opt.down ? b-a : a-b; + }); + } + + // filter duplicate values + array = array.map(v => v.round(5)).filter((e,i,a) => i < 1 || a[i-1] !== a[i]); + + // return array.map(v => Math.abs(parseFloat(v.toFixed(5)))); + return array.map(v => parseFloat(v.toFixed(5))); + } +} + +/** + * given a point, append to the correct + * 'where' objec tarray (on, over or under) + * + * @param {Point} p + * @param {number} z offset + * @param {Obejct} where + */ +function checkOverUnderOn(p, z, where) { + let delta = p.z - z; + if (Math.abs(delta) < config.precision_slice_z) { // on + where.on.push(p); + } else if (delta < 0) { // under + where.under.push(p); + } else { // over + where.over.push(p); + } +} + +/** + * Given a point over and under a z offset, calculate + * and return the intersection point on that z plane + * + * @param {Point} over + * @param {Point} under + * @param {number} z offset + * @returns {Point} intersection point + */ +function intersectPoints(over, under, z) { + let ip = []; + for (let i = 0; i < over.length; i++) { + for (let j = 0; j < under.length; j++) { + ip.push(over[i].intersectZ(under[j], z)); + } + } + return ip; +} + +/** + * Ensure points are unique with a cache/key algorithm + */ +function getCachedPoint(phash, p) { + let cached = phash[p.key]; + if (!cached) { + phash[p.key] = p; + return p; + } + return cached; +} + +/** + * Given two points and hints about their edges, + * return a new Line object with points sorted + * lexicographically by key. This allows for future + * line de-duplication and joins. + * + * @param {Object} phash + * @param {Point} p1 + * @param {Point} p2 + * @param {boolean} [coplanar] + * @param {boolean} [edge] + * @returns {Line} + */ +function makeZLine(phash, p1, p2, coplanar, edge) { + p1 = getCachedPoint(phash, p1.clone()); + p2 = getCachedPoint(phash, p2.clone()); + let line = newOrderedLine(p1,p2); + line.coplanar = coplanar || false; + line.edge = edge || false; + return line; +} + +Slicer.checkOverUnderOn = checkOverUnderOn; +Slicer.intersectPoints = intersectPoints; + +broker.subscribe("minion.started", msg => { + const { funcs, cache, reply, log } = msg; + + funcs.cam_slice_init = () => { + cache.slicer = new Slicer(); + }; + + funcs.cam_slice_cleanup = () => { + delete cache.slicer; + }; + + funcs.cam_slice = (data, seq) => { + const { bucket, opt } = data; + // log({ slice: bucket, opt }); + cache.slicer.sliceBucket(bucket, opt, slice => { + // console.log({ slice }); + }).then(data => { + data.forEach(rec => { + rec.polys = encode(rec.polys); + if (rec.lines) { + const points = rec.lines.map(l => [l.p1, l.p2]).flat(); + rec.lines = encodePointArray(points); + } + }); + reply({ seq, slices: data }); + }); + }; +}); diff --git a/src2/kiri-mode/cam/slicer2.js b/src2/kiri-mode/cam/slicer2.js new file mode 100644 index 00000000..c0a99a15 --- /dev/null +++ b/src2/kiri-mode/cam/slicer2.js @@ -0,0 +1,324 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { base } from '../../../geo/base.js'; +import { kiri } from '../../kiri.js'; +import { config, newOrderedLine, newPoint } from '../../../geo/base.js'; + + +const { base, kiri } = root; +const { config, newOrderedLine, newPoint } = base; + +class Slicer { + + static intersectPoints = intersectPoints; + static checkOverUnderOn = checkOverUnderOn; + static removeDuplicateLines = removeDuplicateLines; + + constructor(index = 0) { + this.min = Infinity; + this.max = -Infinity; + this.index = index; + } + + // position is a non-indexed 3js geometry position attribute array + // create Point array from optionally filtered Z range + setFromArray(position, options = {}) { + const min = options.min || -Infinity; + const max = options.max || Infinity; + const len = position.length; + const pts = this.points = []; + for (let i=0; i max) { + continue; + } + pts.push( + newPoint(x0, y0, z0), + newPoint(x1, y1, z1), + newPoint(x2, y2, z2) + ); + this.min = options.min || Math.min(this.min, minz); + this.max = options.max || Math.max(this.max, maxz); + } + return this; + } + + setFromPoints(points, options = {}) { + const pts = this.points = []; + const len = points.length; + const min = options.min || -Infinity; + const max = options.max || Infinity; + for (let i=0; i max) { + continue; + } + pts.push(p0, p1, p2); + this.min = options.min || Math.min(this.min, minz); + this.max = options.max || Math.max(this.max, maxz); + } + return this; + } + + slice(interval = 1) { + const ret = []; + for (let z = this.min; z < this.max; z += interval) { + ret.push({ z, index: this.index++, lines: this.sliceZ(z) }); + } + return ret; + } + + sliceZ(z, options = {}) { + const points = this.points; + const zMin = this.min; + const zMax = this.max; + const phash = {}; + const lines = []; + + let { under, over, both } = options; + let p1, p2, p3; + + // default to 'over' selection with 2 points on a line + if (!under && !both) over = true; + + // iterate over matching buckets for this z offset + for (let i = 0; i < points.length; ) { + p1 = points[i++]; + p2 = points[i++]; + p3 = points[i++]; + let where = {under: [], over: [], on: []}; + checkOverUnderOn(p1, z, where); + checkOverUnderOn(p2, z, where); + checkOverUnderOn(p3, z, where); + if (where.under.length === 3 || where.over.length === 3) { + // does not intersect (all 3 above or below) + } else if (where.on.length === 2) { + // one side of triangle is on the Z plane and 3rd is below + // drop lines with 3rd above because that leads to ambiguities + // with complex nested polygons on flat surface + let add2 = both || + (over && (where.over.length === 1 || z === zMax)) || + (under && (where.under.length === 1 || z === zMin)); + if (add2) { + lines.push(makeZLine(phash, where.on[0], where.on[1], false, true)); + } + } else if (where.on.length === 3) { + // triangle is coplanar with Z + // we drop these because this face is attached to 3 others + // that will satisfy the if above (line) with 2 points + } else if (where.under.length === 0 || where.over.length === 0) { + // does not intersect but one point is on the slice Z plane + } else { + // compute two point intersections and construct line + let line = intersectPoints(where.over, where.under, z); + if (line.length < 2 && where.on.length === 1) { + line.push(where.on[0]); + } + if (line.length === 2) { + lines.push(makeZLine(phash, line[0], line[1])); + } else { + console.log({msg: "invalid ips", line: line, where: where}); + } + } + } + + if (lines.length == 0 && options.noEmpty) { + return; + } + + return removeDuplicateLines(lines); + } + +} + +/** + * given a point, append to the correct + * 'where' objec tarray (on, over or under) + * + * @param {Point} p + * @param {number} z offset + * @param {Obejct} where + */ +function checkOverUnderOn(p, z, where) { + let delta = p.z - z; + if (Math.abs(delta) < config.precision_slice_z) { // on + where.on.push(p); + } else if (delta < 0) { // under + where.under.push(p); + } else { // over + where.over.push(p); + } +} + +/** + * Given a point over and under a z offset, calculate + * and return the intersection point on that z plane + * + * @param {Point} over + * @param {Point} under + * @param {number} z offset + * @returns {Point} intersection point + */ +function intersectPoints(over, under, z) { + let ip = []; + for (let i = 0; i < over.length; i++) { + for (let j = 0; j < under.length; j++) { + ip.push(over[i].intersectZ(under[j], z)); + } + } + return ip; +} + +/** + * Ensure points are unique with a cache/key algorithm + */ +function getCachedPoint(phash, p) { + let cached = phash[p.key]; + if (!cached) { + phash[p.key] = p; + return p; + } + return cached; +} + +/** + * Given two points and hints about their edges, + * return a new Line object with points sorted + * lexicographically by key. This allows for future + * line de-duplication and joins. + * + * @param {Object} phash + * @param {Point} p1 + * @param {Point} p2 + * @param {boolean} [coplanar] + * @param {boolean} [edge] + * @returns {Line} + */ +function makeZLine(phash, p1, p2, coplanar, edge) { + p1 = getCachedPoint(phash, p1); + p2 = getCachedPoint(phash, p2); + let line = newOrderedLine(p1,p2); + line.coplanar = coplanar || false; + line.edge = edge || false; + return line; +} + +/** + * eliminate duplicate lines and interior-only lines (coplanar) + * + * lines are sorted using lexicographic point keys such that + * they are comparable even if their points are reversed. hinting + * for deletion, co-planar and suspect shared edge is detectable at + * this time. + * + * @param {Line[]} lines + * @returns {Line[]} + */ +function removeDuplicateLines(lines, debug) { + let output = [], + tmplines = [], + points = [], + pmap = {}; + + function cachePoint(p) { + let cp = pmap[p.key]; + if (cp) return cp; + points.push(p); + pmap[p.key] = p; + return p; + } + + function addLinesToPoint(point, line) { + cachePoint(point); + if (!point.group) point.group = [ line ]; + else point.group.push(line); + } + + // mark duplicates for deletion preserving edges + lines.sort(function (l1, l2) { + if (l1.key === l2.key) { + l1.del = !l1.edge; + l2.del = !l2.edge; + if (debug && (l1.del || l2.del)) { + console.log('dup', l1, l2); + } + return 0; + } + return l1.key < l2.key ? -1 : 1; + }); + + // associate points with their lines, cull deleted + lines.forEach(function(line) { + if (!line.del) { + tmplines.push(line); + addLinesToPoint(line.p1, line); + addLinesToPoint(line.p2, line); + } + }); + + // merge collinear lines + points.forEach(function(point) { + if (point.group.length != 2) return; + let l1 = point.group[0], + l2 = point.group[1]; + if (l1.isCollinear(l2)) { + l1.del = true; + l2.del = true; + // find new endpoints that are not shared point + let p1 = l1.p1 != point ? l1.p1 : l1.p2, + p2 = l2.p1 != point ? l2.p1 : l2.p2, + newline = base.newOrderedLine(p1,p2); + // remove deleted lines from associated points + p1.group.remove(l1); + p1.group.remove(l2); + p2.group.remove(l1); + p2.group.remove(l2); + // associate new line with points + p1.group.push(newline); + p2.group.push(newline); + // add new line to lines array + newline.edge = l1.edge || l2.edge; + tmplines.push(newline); + } + }); + + // mark duplicates for deletion + // but preserve one if it's an edge + tmplines.sort(function (l1, l2) { + if (l1.key === l2.key) { + l1.del = true; + l2.del = !l2.edge; + return 0; + } + return l1.key < l2.key ? -1 : 1; + }); + + // create new line array culling deleted + tmplines.forEach(function(line) { + if (!line.del) { + output.push(line); + line.p1.group = null; + line.p2.group = null; + } + }); + + return output; +} + +kiri.topo_slicer = Slicer; + + +export { checkOverUnderOn, intersectPoints, getCachedPoint, makeZLine, removeDuplicateLines, cachePoint, addLinesToPoint, min, max, len, pts, i, x0, y0, z0, x1, y1, z1, x2, y2, z2, minz, maxz, p0, p1, p2, ret, z, points, zMin, zMax, phash, lines, where, add2, line, delta, ip, j, cached, output, cp, l1, Slicer }; diff --git a/src2/kiri-mode/cam/tool.js b/src2/kiri-mode/cam/tool.js new file mode 100644 index 00000000..b4aab164 --- /dev/null +++ b/src2/kiri-mode/cam/tool.js @@ -0,0 +1,208 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +const HPI = Math.PI/2; +const RAD2DEG = 180/Math.PI; + +class Tool { + constructor(settings, id, number) { + if (number >= 0) { + this.tool = settings.tools.filter(tool => tool.number == number)[0]; + } else { + this.tool = settings.tools.filter(tool => tool.id == id)[0]; + } + if (!this.tool) { + this.tool = Object.assign({}, settings.tools[0]); + this.tool.number = number >= 0 ? number : this.tool.number; + this.tool.id = id >= 0 ? id : this.tool.id; + } + } + + getID() { + return this.tool.id; + } + + getName() { + return this.tool.name; + } + + getType() { + return this.tool.type; + } + + getNumber() { + return this.tool.number; + } + + isMetric() { + return this.tool.metric; + } + + unitScale() { + return this.isMetric() ? 1 : 25.4; + } + + fluteLength() { + return this.unitScale() * this.tool.flute_len; + } + + fluteDiameter() { + return this.unitScale() * this.tool.flute_diam; + } + + tipDiameter() { + return this.unitScale() * this.tool.taper_tip; + } + + maxDiameter() { + return Math.max(this.fluteDiameter(), this.tipDiameter(), this.shaftDiameter()); + } + + traceOffset() { + return (this.hasTaper() ? this.tipDiameter() : this.fluteDiameter()) / 2; + } + + contourOffset(step) { + const diam = Math.min(this.hasTaper() ? this.tipDiameter() : this.fluteDiameter()); + return diam ? diam * step : step; + } + + setTaperLengthFromAngle(angle) { + const rad = (this.flute_diam - this.taper_tip) / 2; + return this.flute_len = calcTaperLength(rad, angle); + } + + getTaperAngle() { + return calcTaperAngle((this.flute_diam - this.taper_tip) / 2, this.flute_len); + } + + shaftLength() { + return this.unitScale() * this.tool.shaft_len; + } + + /** + * returns the length of the drill tip as a function of the flute diameter, + * given a 118 deg point angle. the result is in workspace units. + * + * @return {number} length of the drill tip. + */ + drillTipLength() { + const drillAngleRad = 140 * Math.PI / 180; + return 0.5 * this.fluteDiameter() * Math.sin(drillAngleRad); + } + + shaftDiameter() { + return this.unitScale() * this.tool.shaft_diam; + } + + isBallMill() { + return this.tool.type === "ballmill"; + } + + isTaperMill() { + return this.tool.type === "tapermill"; + } + + isDrill(){ + return this.tool.type === "drill"; + } + + hasTaper() { + return this.isTaperMill(); + } + + /** + * generate tool profile with resolution number of pixels. the result is a + * Float32Array stored in `this.profile` and `this.profileDim` containing + * dimensions of the profile in pixels and shared array buffer size. + * + * @param {number} resolution - number of pixels for the tool profile + * + * @return {Tool} this + */ + generateProfile(resolution) { + // generate tool profile + let ball = this.isBallMill(), + taper = this.hasTaper(), + drill = this.isDrill(), + tip_diameter = this.tipDiameter(), + drill_tip_length = this.drillTipLength(), + shaft_offset = this.fluteLength(), + flute_diameter = this.fluteDiameter(), + shaft_diameter = Math.max(flute_diameter, this.shaftDiameter()), + max_diameter = Math.max(flute_diameter, shaft_diameter), + shaft_pix_float = max_diameter / resolution, + shaft_pix_int = Math.round(shaft_pix_float), + shaft_radius_pix_float = shaft_pix_float / 2, + flute_radius = flute_diameter / 2, + flute_pix_float = flute_diameter / resolution, + flute_radius_pix_float = flute_pix_float / 2, + tip_pix_float = tip_diameter / resolution, + tip_radius_pix_float = tip_pix_float / 2, + tip_max_radius_offset = flute_radius_pix_float - tip_radius_pix_float, + profile_pix_iter = shaft_pix_int + (1 - shaft_pix_int % 2), + toolCenter = (shaft_pix_int - (shaft_pix_int % 2)) / 2, + toolOffset = [], + larger_shaft = shaft_diameter - flute_diameter > 0.001, + rpixsq = flute_radius_pix_float * flute_radius_pix_float, + maxo = -Infinity; + + // for each point in tool profile, check inside radius + for (let x = 0; x < profile_pix_iter; x++) { + for (let y = 0; y < profile_pix_iter; y++) { + let dx = x - toolCenter, + dy = y - toolCenter, + dist_from_center = Math.sqrt(dx * dx + dy * dy); + if (dist_from_center <= flute_radius_pix_float) { // if xy point inside flute radius + maxo = Math.max(maxo, dx, dy); + // flute offset points + let z_offset = 0; + if (ball) { + let rd = dist_from_center * dist_from_center; + z_offset = Math.sqrt(rpixsq - rd) * resolution - flute_radius; + // z_offset = (1 - Math.cos((dist_from_center / flute_radius_pix_float) * HPI)) * -flute_radius; + } else if (taper && dist_from_center >= tip_radius_pix_float) {// if tapered and not in the flat tip radius + z_offset = ((dist_from_center - tip_radius_pix_float) / tip_max_radius_offset) * -shaft_offset; + }else if(drill){ + z_offset = -dist_from_center /45; + } + toolOffset.push(dx, dy, z_offset); + } else if (shaft_offset && larger_shaft && dist_from_center <= shaft_radius_pix_float) { + // shaft offset points + toolOffset.push(dx, dy, -shaft_offset); + } + } + } + + // convert to shared array for use with minions + const profile = new Float32Array(new SharedArrayBuffer(toolOffset.length * 4)); + profile.set(toolOffset); + + this.profile = profile; + this.profileDim = { + size: shaft_diameter, + pix: profile_pix_iter + 2, + maxo + }; + + return this; + } +} + +function calcTaperAngle(rad, len) { + return (Math.atan(rad / len) * RAD2DEG); +}; + +function calcTaperLength(rad, angle) { + return (rad / Math.tan(angle)); +}; + +function getToolDiameter(settings, id) { + return new Tool(settings, id).fluteDiameter(); +}; + +export { + Tool, + calcTaperAngle, + calcTaperLength, + getToolDiameter +}; diff --git a/src2/kiri-mode/cam/tools.js b/src2/kiri-mode/cam/tools.js new file mode 100644 index 00000000..eeaaf31c --- /dev/null +++ b/src2/kiri-mode/cam/tools.js @@ -0,0 +1,354 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { api } from '../kiri/api.js'; +import { consts } from '../kiri/consts.js'; +import { settings } from '../kiri/settings.js'; + +// Convert root destructuring to imports +import { kiri } from '../kiri.js'; + + + const DEG2RAD = Math.PI / 180; + let { kiri } = root, + { api, consts } = kiri, + { uc, ui } = api, + { MODES } = consts, + DOC = document, + selectedTool = null, + editTools = null, + maxTool = 0, + toolNames = ['endmill','ballmill','tapermill','drill']; + + api.show.tools = showTools; + + // extend API + Object.assign(api.tool, { + update: updateTool + }); + + function settings() { + return api.conf.get(); + } + + function renderTools() { + ui.toolSelect.innerHTML = ''; + maxTool = 0; + editTools.forEach(function(tool, index) { + maxTool = Math.max(maxTool, tool.number); + tool.order = index; + let opt = DOC.createElement('option'); + opt.appendChild(DOC.createTextNode(tool.name)); + opt.onclick = function() { selectTool(tool) }; + ui.toolSelect.appendChild(opt); + }); + } + + + + function selectTool(tool) { + selectedTool = tool; + ui.toolName.value = tool.name; + ui.toolNum.value = tool.number; + ui.toolFluteDiam.value = tool.flute_diam; + ui.toolFluteLen.value = tool.flute_len; + ui.toolShaftDiam.value = tool.shaft_diam; + ui.toolShaftLen.value = tool.shaft_len; + ui.toolTaperTip.value = tool.taper_tip || 0; + ui.toolMetric.checked = tool.metric; + ui.toolType.selectedIndex = toolNames.indexOf(tool.type); + if (tool === 'tapermill') { + ui.toolTaperAngle.value = kiri.driver.CAM.calcTaperAngle( + (tool.flute_diam - tool.taper_tip) / 2, tool.flute_len + ).round(1); + } else if(tool === 'drill'){ + ui.toolTaperAngle.value = 118; + + } else { + ui.toolTaperAngle.value = 0; + } + renderTool(tool); + } + + function otag(o) { + if (Array.isArray(o)) { + let out = [] + o.forEach(oe => out.push(otag(oe))); + return out.join(''); + } + let tags = []; + Object.keys(o).forEach(key => { + let val = o[key]; + let att = []; + Object.keys(val).forEach(tk => { + let tv = val[tk]; + att.push(`${tk.replace(/_/g,'-')}="${tv}"`); + }); + tags.push(`<${key} ${att.join(' ')}>`); + }); + return tags.join(''); + } + + function renderTool(tool) { + let type = selectedTool.type; + let taper = type=== 'tapermill' + let drill = type === 'drill' + const drillAngleRad = 140 * Math.PI / 180 + ui.toolTaperAngle.disabled = taper ? undefined : 'true'; + ui.toolTaperTip.disabled = taper ? undefined : 'true'; + $('tool-view').innerHTML = ''; + setTimeout(() => { + let svg = $('tool-svg'), + pad = 10, + dim = { w: svg.clientWidth, h: svg.clientHeight }, + max = { w: dim.w - pad * 2, h: dim.h - pad * 2}, + off = { x: pad, y: pad }, + isBall = type === "ballmill", + shaft_fill = "#cccccc", + flute_fill = "#dddddd", + stroke = "#777777", + stroke_width = 3, + stroke_thin = stroke_width / 2, + shaft = tool.shaft_len || 1, + flute = tool.flute_len || 1, + drillTip = drill ? 0.5 * tool.flute_diam * Math.sin(drillAngleRad) : 0, + total_len = shaft + flute+drillTip, + units = dim.h / total_len, + shaft_len = (shaft / total_len) * max.h, + flute_len = (flute / total_len) * max.h, + drill_tip_len = (drillTip / total_len) * max.h, + shaft_diam = tool.shaft_diam * units, + flute_diam = tool.flute_diam * units, + // total_wid = Math.max(flute_diam, shaft_diam), + shaft_off = (max.w - shaft_diam) / 2, + flute_off = (max.w - flute_diam) / 2, + taper_off = (max.w - (tool.taper_tip || 0) * units) / 2, + parts = [ + // shaft rectangle + { rect: { + x: off.x + shaft_off, + y: off.y, + width: max.w - shaft_off * 2, + height: shaft_len, + fill: shaft_fill, + stroke_width, + stroke + } } + ]; + if (taper) { + let yoff = off.y + shaft_len; + // let mid = dim.w / 2; + parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[ + `M ${off.x + flute_off} ${yoff}`, + `L ${off.x + taper_off} ${yoff + flute_len}`, + `L ${dim.w - off.x - taper_off} ${yoff + flute_len}`, + `L ${dim.w - off.x - flute_off} ${yoff}`, + `z` + ].join('\n')}}); + } else if(drill){ + + const x1 = off.x + flute_off, + y1 = off.y + shaft_len, + x2 = dim.w - off.x - flute_off, + y2 = y1 + flute_len, + xMid = dim.w / 2 + + parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[ + `M ${x1} ${y1}`, //move to top left + `L ${x1} ${y2}`, //line to bottom left + `L ${xMid} ${y2+drill_tip_len}`, //line to bottom mid point + `L ${x2} ${y2}`, //line to bottom right + `L ${x2} ${y1}`, //line to top right + `z` + ].join('\n')}}); + //add drill flute lines + parts.push({ line: { + x1, y1, x2, y2: (y1 + y2) / 2, + stroke, stroke_width: stroke_thin + } }); + parts.push({ line: { + x1, y1: (y1 + y2) / 2, x2, y2, + stroke, stroke_width: stroke_thin + } }); + } else { + let fl = isBall ? flute_len - flute_diam/2 : flute_len; + let x1 = off.x + flute_off; + let y1 = off.y + shaft_len; + let x2 = x1 + max.w - flute_off * 2; + let y2 = y1 + fl; + // flute rectangle + parts.push({ rect: { + x: off.x + flute_off, + y: off.y + shaft_len, + width: max.w - flute_off * 2, + height: fl, + fill: flute_fill, + stroke_width, + stroke, + } }); + // hatch "fill" flute + parts.push({ line: { x1, y1, x2, y2, stroke, stroke_width: stroke_thin } }); + parts.push({ line: { + x1: (x1 + x2) / 2, y1, x2, y2: (y1 + y2) / 2, + stroke, stroke_width: stroke_thin + } }); + parts.push({ line: { + x1, y1: (y1 + y2) / 2, x2: (x1 + x2) / 2, y2, + stroke, stroke_width: stroke_thin + } }); + } + if (isBall) { + let rad = (max.w - flute_off * 2) / 2; + let xend = dim.w - off.x - flute_off; + let yoff = off.y + shaft_len + flute_len + stroke_width/2 - flute_diam/2; + parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[ + `M ${off.x + flute_off} ${yoff}`, + `A ${rad} ${rad} 0 0 0 ${xend} ${yoff}`, + // `L ${off.x + flute_off} ${yoff}` + ].join('\n')}}) + } + svg.innerHTML = otag(parts); + }, 10); + } + + function updateTool(ev) { + selectedTool.name = ui.toolName.value; + selectedTool.number = parseInt(ui.toolNum.value); + selectedTool.flute_diam = parseFloat(ui.toolFluteDiam.value); + selectedTool.flute_len = parseFloat(ui.toolFluteLen.value); + selectedTool.shaft_diam = parseFloat(ui.toolShaftDiam.value); + selectedTool.shaft_len = parseFloat(ui.toolShaftLen.value); + selectedTool.taper_tip = parseFloat(ui.toolTaperTip.value); + selectedTool.metric = ui.toolMetric.checked; + selectedTool.type = toolNames[ui.toolType.selectedIndex]; + if (selectedTool.type === 'tapermill') { + const CAM = kiri.driver.CAM; + const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2; + if (ev && ev.target === ui.toolTaperAngle) { + const angle = parseFloat(ev.target.value); + const len = CAM.calcTaperLength(rad, angle * DEG2RAD); + selectedTool.flute_len = len; + ui.toolTaperAngle.value = angle.round(1); + ui.toolFluteLen.value = selectedTool.flute_len.round(4); + } else { + ui.toolTaperAngle.value = CAM.calcTaperAngle(rad, selectedTool.flute_len).round(1); + } + } else { + ui.toolTaperAngle.value = 0; + } + renderTools(); + ui.toolSelect.selectedIndex = selectedTool.order; + setToolChanged(true); + renderTool(selectedTool); + } + + function setToolChanged(changed) { + editTools.changed = changed; + ui.toolsSave.disabled = !changed; + } + + function showTools() { + if (api.mode.get_id() !== MODES.CAM) return; + api.settings.sync.get().then(_showTools); + } + + function _showTools() { + let selectedIndex = null; + + editTools = settings().tools.slice().sort((a,b) => { + return a.name > b.name ? 1 : -1; + }); + + setToolChanged(false); + + ui.toolsClose.onclick = function() { + if (editTools.changed && !confirm("abandon changes?")) return; + api.dialog.hide(); + }; + ui.toolAdd.onclick = function() { + let metric = settings().controller.units === 'mm'; + editTools.push(Object.assign({ + id: Date.now(), + number: maxTool + 1, + name: "new tool", + type: "endmill", + taper_tip: 0, + metric + }, metric ? { + shaft_diam: 2, + shaft_len: 15, + flute_diam: 2, + flute_len: 20, + } : { + shaft_diam: 0.25, + shaft_len: 1.5, + flute_diam: 0.25, + flute_len: 2, + })); + setToolChanged(true); + renderTools(); + ui.toolSelect.selectedIndex = editTools.length-1; + selectTool(editTools[editTools.length-1]); + }; + ui.toolCopy.onclick = function() { + let clone = Object.assign({}, selectedTool); + let { name } = clone; + let split = name.split(' '); + let endv = parseInt(split.pop()); + if (endv) { + name = split.join(' ') + ' ' + (endv + 1); + } else { + name = `${name} 2`; + } + clone.id = Date.now(); + clone.number = maxTool + 1; + clone.name = name; + editTools.push(clone); + setToolChanged(true); + renderTools(); + ui.toolSelect.selectedIndex = editTools.length-1; + selectTool(editTools[editTools.length-1]); + }; + ui.toolDelete.onclick = function() { + editTools.remove(selectedTool); + setToolChanged(true); + renderTools(); + }; + ui.toolsSave.onclick = function() { + if (selectedTool) updateTool(); + settings().tools = editTools.sort((a,b) => { + return a.name < b.name ? -1 : 1; + }); + setToolChanged(false); + api.conf.save(); + api.conf.update_fields(); + api.event.settings(); + api.settings.sync.put(); + }; + ui.toolsImport.onclick = (ev) => api.event.import(ev); + ui.toolsExport.onclick = () => { + uc.prompt("Export Tools Filename", "tools").then(name => { + if (!name) { + return; + } + const record = { + version: kiri.version, + tools: api.conf.get().tools, + time: Date.now() + }; + api.util.download(api.util.b64enc(record), `${name}.km`); + }); + }; + + renderTools(); + if (editTools.length > 0) { + selectTool(editTools[0]); + ui.toolSelect.selectedIndex = 0; + } else { + ui.toolAdd.onclick(); + } + + api.dialog.show('tools'); + ui.toolSelect.focus(); + } + + +export { settings, renderTools, selectTool, otag, renderTool, updateTool, setToolChanged, showTools, DEG2RAD, opt, out, tags, att, tv, type, taper, drill, drillAngleRad, svg, yoff, mid, x1, fl, y1, x2, y2, rad, xend, CAM, angle, len, selectedIndex, metric, clone, split, endv, record }; diff --git a/src2/kiri-mode/cam/topo3.js b/src2/kiri-mode/cam/topo3.js new file mode 100644 index 00000000..45c2e282 --- /dev/null +++ b/src2/kiri-mode/cam/topo3.js @@ -0,0 +1,894 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { base } from '../../../geo/base.js'; +import { kiri } from '../../kiri.js'; +import { moto } from '../../../moto.js'; +import { driver, newSlice } from '../../kiri.js'; +import { CAM } from './driver.js'; +import { polygons, newLine, newSlope, newPoint, newPolygon } from '../../../geo/base.js'; + +const { base, kiri, moto } = root; +const { driver, newSlice } = kiri; +const { CAM } = driver; +const { polygons, newLine, newSlope, newPoint, newPolygon } = base; + +const PRO = CAM.process; +const POLY = polygons; +const RAD2DEG = 180 / Math.PI; +const DEG2RAD = Math.PI / 180; + +class Topo { + constructor() { } + + async generate(opt = {}) { + let { state, contour, onupdate, ondone } = opt; + let { widget, settings, tshadow, center, tabs, color } = opt.state; + + let { controller, process } = settings, + animesh = parseInt(controller.animesh || 100) * 2500, + axis = contour.axis.toLowerCase(), + contourX = axis === "x", + contourY = axis === "y", + bounds = widget.getBoundingBox().clone(), + tolerance = contour.tolerance, + flatness = contour.flatness || (tolerance / 100), + shadow = tshadow, + minX = bounds.min.x, + maxX = bounds.max.x, + minY = bounds.min.y, + maxY = bounds.max.y, + zBottom = contour.bottom ? state.zBottom : 0, + zMin = Math.max(bounds.min.z, zBottom) + 0.0001, + boundsX = maxX - minX, + boundsY = maxY - minY, + inside = contour.inside, + density = 1 + (contour.reduction || 0), + resolution = tolerance ? tolerance : 1/Math.sqrt(animesh/(boundsX * boundsY)), + tool = new CAM.Tool(settings, contour.tool), + toolOffset = tool.generateProfile(resolution).profile, + toolDiameter = tool.fluteDiameter(), + toolStep = toolDiameter * contour.step, //tool.contourOffset(contour.step), + leave = contour.leave || 0, + maxangle = contour.angle, + curvesOnly = contour.curves, + bridge = contour.bridging || 0, + // R2A = 180 / Math.PI, + stepsX = Math.ceil(boundsX / resolution), + stepsY = Math.ceil(boundsY / resolution), + widtopo = widget.topo, + topoCache = widtopo + && widtopo.resolution === resolution + && widtopo.diameter === toolDiameter + ? widtopo : undefined, + topo = widget.topo = topoCache || { + data: new Float32Array(new SharedArrayBuffer(stepsX * stepsY * 4)), + stepsX: stepsX, + stepsY: stepsY, + bounds: bounds, + diameter: toolDiameter, + resolution: resolution, + profile: toolOffset, + widget: widget, + raster: true, + slices: null + }, + data = topo.data, + newslices = [], + tabsMax = tabs ? Math.max(...tabs.map(tab => tab.dim.z/2 + tab.pos.z)) : 0, + tabsOn = tabs, + tabHeight = Math.max(process.camTabsHeight + zBottom, tabsMax), + clipTab = tabsOn ? [] : null, + clipTo = inside ? shadow : POLY.expand(shadow, toolDiameter/2 + resolution * 3), + partOff = inside ? 0 : toolDiameter / 2 + resolution, + gridDelta = Math.floor(partOff / resolution), + debug = false, + debug_clips = debug && true, + debug_topo = debug && true, + debug_topo_lines = debug && true, + debug_topo_shells = debug && true, + time = Date.now; + + if (tolerance === 0 && !topoCache) { + console.log(widget.id, 'topo auto tolerance', resolution.round(4)); + } + + // console.log({ resolution, flatness }); + // used by Pocket -> Contour + this.tolerance = resolution; + + if (tabs) { + clipTab.appendAll(tabs.map(tab => { + let ctab = POLY.expand([tab.poly], toolDiameter/2); + ctab.forEach(ct => ct.z = tab.dim.z/2 + tab.pos.z); + return ctab; + }).flat()); + } + + // debug clipTab and clipTo + if (debug_clips) { + const debug = newSlice(-1); + const output = debug.output(); + // if (clipTab) output.setLayer("clip.tab", { line: 0xff0000 }).addPolys(clipTab); + if (clipTo) output.setLayer("clip.to", { line: 0x00dd00 }).addPolys(clipTo); + newslices.push(debug); + } + + const probe = this.probe = new Probe({ + profile: toolOffset, + data, + stepsX, + stepsY, + boundsX, + boundsY, + minX, + minY, + zMin + }); + + const { toolAtZ, toolAtXY, zAtXY } = probe; + + this.toolAtZ = toolAtZ; + this.toolAtXY = toolAtXY; + this.zAtXY = zAtXY; + + const trace = this.trace = new Trace(probe, { + curvesOnly, + maxangle, + flatness, + bridge, + contourX + }); + + if (topo.raster) { + const box = topo.box = new THREE.Box2(); + + const params = { + resolution, + curvesOnly, + flatness, + stepsY, + minY, + maxY, + zMin, + data, + box + }; + + onupdate(0, 1, "raster"); + await this.raster(widget, params, (i, l, p) => { + onupdate(i / 2, l, p); + }); + topo.raster = false; + } + + await this.contour({ + box: topo.box, + minX, + maxX, + minY, + maxY, + stepsX, + stepsY, + boundsX, + boundsY, + partOff, + gridDelta, + resolution, + toolStep, + contourX, + contourY, + density, + clipTo, + clipTab, + clipTabZ: clipTab ? clipTab.map(t => t.z) : undefined, + tabHeight, + newslices, + leave, + color + }, (i, l, p) => { + onupdate(l / 2 + i / 2, l, p); + }); + + ondone(newslices); + + return this; + } + + async raster(widget, params, onupdate) { + const { resolution } = params; + const { box } = params; + const { worker, minions } = kiri; + + const vertices = widget.getGeoVertices({ unroll: true, translate: true }).toShared(); + const range = { min: Infinity, max: -Infinity }; + + // swap XZ in shared array + for (let i=0,l=vertices.length; i slice.max) { + slices.push(slice); + slice = { min: z, max: z + step, index }; + } + index++; + } + slices.push(slice); + // console.log({ shards, range, step, slices }); + + let complete = 0; + // define sharded ranges + if (minions.running > 1) { + + worker.putCache({ key: widget.id, data: vertices }, { done: data => { + // console.log({ put_cache_done: data }); + }}); + + let promises = slices.map(slice => { + return new Promise(resolve => { + minions.queue({ + cmd: "topo_raster", + id: widget.id, + params, + slice + }, data => { + resolve(data); + onupdate(++complete, slices.length, "raster"); + }); + }); + }); + + // merge boxes for all rasters for contouring clipping + (await Promise.all(promises)) + .map(rec => rec.box) + .map(box => new THREE.Box2( + new THREE.Vector2(box.min.x, box.min.y), + new THREE.Vector2(box.max.x, box.max.y) + )) + .map(box2 => { + box.union(box2); + return box2; + }); + + worker.clearCache({}, { done: data => { + // console.log({ clear_cache_done: data }); + }}); + + } else { + + // iterate over shards, merge output + // const output = []; + for (let slice of slices) { + new kiri.topo_slicer(slice.index) + .setFromArray(vertices, slice) + .slice(resolution) + .map(rec => { + + const slice = kiri.newSlice(rec.z); + slice.index = rec.index; + slice.lines = rec.lines; + for (let line of rec.lines) { + const { p1, p2 } = line; + if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } + if (!p2.swapped) { p2.swapXZ(); p2.swapped = true } + } + + raster_slice({ + ...params, + box, + lines: rec.lines, + gridx: rec.index + }); + + return slice; + }); + onupdate(++complete, slices.length, "raster"); + } + + } + } + + async contour(params, onupdate) { + const trace = this.trace; + const concurrent = kiri.minions.running; + + const { minX, maxX, minY, maxY, boundsX, boundsY, stepsX, stepsY } = params; + const { gridDelta, resolution, density, partOff, toolStep, contourX, contourY } = params; + const { clipTo, clipTab, clipTabZ, tabHeight, newslices, color, leave } = params; + + let stepsTaken = 0, + stepsTotal = 0; + + if (contourX) { + stepsTotal += ((maxY - minY + partOff * 2) / toolStep) | 0; + } + + if (contourY) { + stepsTotal += ((maxX - minX + partOff * 2) / toolStep) | 0; + } + + if (stepsTotal === 0) { + return; + } + + const box = params.box.clone().expandByVector(new THREE.Vector3( + partOff, partOff, 0 + )); + + trace.init({ + box, + leave, + clipTo, + clipTab, + clipTabZ, + tabHeight, + resolution, + concurrent, + density + }); + + let resolver; + let pcount = 0; + let slicesY = []; + let slicesX = []; + let promise = new Promise(resolve => { + resolver = () => { + // sort output slices (required for async) + slicesY.sort((a,b) => a.z - b.z); + slicesX.sort((a,b) => a.z - b.z); + newslices.appendAll(slicesY); + newslices.appendAll(slicesX); + resolve(); + } + }); + let inc = () => { pcount++ }; + let dec = () => { if (--pcount === 0 && concurrent) resolver() }; + + if (contourY) { + onupdate(0, stepsTotal, "contour y"); + // emit slice per X + for (let x = minX - partOff; x <= maxX + partOff; x += toolStep) { + if (x < box.min.x || x > box.max.x) continue; + const gridx = Math.round(((x - minX) / boundsX) * stepsX); + const gridy = -gridDelta; + inc(); + trace.crossY({ + from: minY - partOff, + to: maxY + partOff, + x, + gridx, + gridy + }, segments => { + if (segments.length > 0) { + let slice = newSlice(gridx); + slice.camLines = segments; + slice.output() + .setLayer("contour y", {face: color, line: color}) + .addPolys(segments); + slicesY.push(slice); + } + onupdate(++stepsTaken, stepsTotal, "contour y"); + dec(); + }); + } + } + + if (contourX) { + // emit slice per Y + onupdate(0, stepsTotal, "contour x"); + for (let y = minY - partOff; y <= maxY + partOff; y += toolStep) { + if (y < box.min.y || y > box.max.y) continue; + const gridy = Math.round(((y - minY) / boundsY) * stepsY); + const gridx = -gridDelta; + inc(); + trace.crossX({ + from: minX - partOff, + to: maxX + partOff, + y, + gridx, + gridy + }, segments => { + if (segments.length > 0) { + let slice = newSlice(gridy); + slice.camLines = segments; + slice.output() + .setLayer("contour x", {face: color, line: color}) + .addPolys(segments); + slicesX.push(slice); + } + onupdate(++stepsTaken, stepsTotal, "contour x"); + dec(); + }); + } + } + + if (!concurrent) resolver(); + + await promise; + + // const lines = newslices.map(s => (s.camLines || []).map(p => p.length)); + // const points = lines.flat().reduce((a,b) => a+b); + // console.log({ lines, points }); + + trace.cleanup(); + } + +} + +class Probe { + + constructor(params) { + + const { data, profile } = params; + const { stepsX, stepsY, boundsX, zMin, minX, minY } = params; + + this.params = params; + + // return the touching z given topo x,y and a tool profile + const toolAtZ = this.toolAtZ = function(x,y) { + let sx = stepsX, + sy = stepsY, + xl = sx - 1, + yl = sy - 1; + + let gv, i = 0, mz = -Infinity; + + while (i < profile.length) { + // tool profile point x, y, and z offsets + const tx = profile[i++] + x; + const ty = profile[i++] + y; + const tz = profile[i++]; + if (tx < 0 || tx > xl || ty < 0 || ty > yl) { + // if outside max topo steps, use zMin + gv = zMin; + } else { + // lookup grid value @ tx, ty + gv = data[tx * sy + ty] || zMin; + } + // update the rest + mz = Math.max(tz + gv, mz); + } + + return Math.max(mz, zMin); + } + + // export z probe function + const rx = stepsX / boundsX; + const ry = stepsX / boundsX; + const toolAtXY = this.toolAtXY = function(px, py) { + px = Math.round(rx * (px - minX)); + py = Math.round(ry * (py - minY)); + return toolAtZ(px, py); + }; + + const zAtXY = this.zAtXY = function(px, py) { + let ix = Math.round(rx * (px - minX)); + let iy = Math.round(ry * (py - minY)); + return data[ix * stepsY + iy] || zMin; + }; + + } + +} + +class Trace { + + constructor(probe, params) { + + const { curvesOnly, maxangle, flatness, bridge, contourX, leave } = params; + + this.params = params; + this.probe = probe; + + let trace, + slice, + latent, + lastPP, + lastSlope; + + const newslice = this.newslice = () => { + this.slice = slice = []; + } + + const newtrace = this.newtrace = function() { + trace = newPolygon().setOpen(); + } + + const end_poly = this.end_poly = function(point) { + if (latent) { + trace.push(latent); + } + if (trace.length > 0) { + // add additional constraint on min perimeter() + if (trace.length > 1) { + slice.push(trace); + } + newtrace(); + } + lastPP = undefined; + latent = undefined; + lastSlope = undefined; + if (point) { + trace.push(point); + lastPP = point; + } + } + + const log = function(map) { + for (let key in map) { + const val = map[key]; + if (typeof val === 'number') { + map[key] = val.round(4); + } + } + console.log(...arguments); + } + + const push_point = this.push_point = function(x, y, z) { + const newP = newPoint(x, y, z); + const lastP = lastPP;//trace.last(); + + if (lastP) { + const dl = (x - lastP.x) || (y - lastP.y); + const dz = z - lastP.z; + const slope = Math.atan2(dz, dl); + if (curvesOnly && Math.abs(dz) < flatness) { + end_poly(newP); + } else if (lastSlope !== undefined && Math.abs(lastSlope - slope) < 0.001) { + latent = newP; + } else { + if (latent) { + trace.push(latent); + latent = undefined; + } + if (curvesOnly) { + const dv = contourX ? Math.abs(lastP.x - x) : Math.abs(lastP.y - y); + // const dz = lastPP.z - z; + const angle = Math.atan2( Math.abs(dz), dv) * RAD2DEG; + if (angle > maxangle) { + end_poly(); + } + } + trace.push(newP); + } + lastSlope = slope; + } else { + trace.push(newP); + } + + lastPP = newP; + } + + const object = this.object = this; + + this.inClip = function (clips, checkZ, point) { + for (let i=0; i { + then(kiri.codec.decode(data.slice)); + }); + } else { + this.crossY_sync(params, then); + } + } + + crossX(params, then) { + const { minions } = kiri; + + if (minions && this.cross.concurrent) { + minions.queue({ + cmd: "trace_x", + params + }, data => { + then(kiri.codec.decode(data.slice)); + }); + } else { + this.crossX_sync(params, then); + } + } + + crossY_sync(params, then) { + const { push_point, end_poly, newtrace, newslice, inClip } = this.object; + const { clipTab, tabHeight, clipTo, box, resolution, density, leave } = this.cross; + const { toolAtZ } = this.probe; + + let { from, to, x, gridx, gridy } = params; + + const step = resolution * density; + const checkr = newPoint(0,0); + newslice(); + newtrace(); + for (let y = from; y < to; y += step) { + if (y < box.min.y || y > box.max.y) { + gridy += density; + continue; + } + // find tool z at grid point + let tv = toolAtZ(gridx, gridy); + checkr.x = x; + checkr.y = y; + // when tabs are on and this point is inside the + // tab polygon, ensure z is at least tabHeight + if (clipTab && tv < tabHeight && inClip(clipTab, tv, checkr)) { + tv = this.tabZ; + } + // if the value is on the floor and inside the clip + // poly (usually shadow), end the segment + if (clipTo && !inClip(clipTo, undefined, checkr)) { + end_poly(); + gridy += density; + continue; + } + push_point(x, y, tv + leave); + gridy += density; + } + end_poly(); + then(this.slice); + } + + crossX_sync(params, then) { + const { push_point, end_poly, newtrace, newslice, inClip } = this.object; + const { clipTab, tabHeight, clipTo, box, resolution, density, leave } = this.cross; + const { toolAtZ } = this.probe; + let { from, to, y, gridx, gridy } = params; + + const step = resolution * density; + const checkr = newPoint(0,0); + newslice(); + newtrace(); + for (let x = from; x < to; x += step) { + if (x < box.min.x || x > box.max.x) { + gridx += density; + continue; + } + // find tool z at grid point + let tv = toolAtZ(gridx, gridy); + checkr.x = x; + checkr.y = y; + // when tabs are on and this point is inside the + // tab polygon, ensure z is at least tabHeight + if (clipTab && tv < tabHeight && inClip(clipTab, tv, checkr)) { + tv = this.tabZ; + } + // if the value is on the floor and inside the clip + // poly (usually shadow), end the segment + if (clipTo && !inClip(clipTo, undefined, checkr)) { + end_poly(); + gridx += density; + continue; + } + push_point(x, y, tv + leave); + gridx += density; + } + end_poly(); + then(this.slice); + } +} + +function raster_slice(inputs) { + const { lines, data, box, resolution, curvesOnly } = inputs; + const { flatness, zMin, minY, maxY, stepsY, gridx } = inputs; + const { slice } = inputs; + + let gridy, + gridi, // index + gridv, // value + i, il, j, x, y, tv; + + // filter lines pairs to only surface "up-facing", "uncovered" lines + let points = []; + // emit an array of valid line-pairs + const len = lines.length; + + outer: for (let i=0; i p2.y) { const tp = p1; p1 = p2; p2 = tp }; + // eliminate if points "under" other lines + for (let j=0; j= i-1 && j <= i+1) continue; + let l2 = lines[j], p3 = l2.p1, p4 = l2.p2; + // sort p3,p4 by y for comparison + if (p3.y > p4.y) { const tp = p3; p3 = p4; p4 = tp }; + // it's under the other line + if (Math.max(p1.z,p2.z) < Math.min(p3.z,p4.z)) { + // it's inside the other line, too, so skip + if (p1.y >= p3.y && p2.y <= p4.y) continue outer; + } + } + points.push(p1, p2); + box.expandByPoint(p1); + box.expandByPoint(p2); + } + + gridy = 0; + // rasterize one x slice + for (y = minY; y < maxY && gridy < stepsY; y += resolution) { + gridi = gridx * stepsY + gridy; + gridv = data[gridi] || zMin; + // strategy using raw lines (faster slice, but more lines) + for (i=0, il=points.length; i gridv || p2.z > gridv); + // segment crosses grid y + const spansy = (p1.y <= y && p2.y >= y); + if (crossz && spansy) { + // compute intersection of z ray up + // and segment at this grid point + const dy = p1.y - p2.y, + dz = p1.z - p2.z, + pct = (p1.y - y) / dy, + nz = p1.z - (dz * pct); + // save if point is greater than existing grid point + if (nz > gridv) { + gridv = data[gridi] = Math.max(nz, zMin); + if (slice) slice.output() + .setLayer("heights", {face: 0, line: 0}) + .addLine( + newPoint(p1.x, y, 0), + newPoint(p1.x, y, gridv) + ); + } + } + } + gridy++; + } + + // remove flat lines when curvesOnly + if (curvesOnly) { + let nup = []; + for (let i=0, p=points, l=p.length; i= flatness) { + nup.push(p1, p2); + } + } + points = nup; + } + + return points; +}; + +CAM.Topo = async function(opt) { + return new Topo().generate(opt); +}; + +moto.broker.subscribe("minion.started", msg => { + const { funcs, cache, reply, log } = msg; + + funcs.topo_raster = (data, seq) => { + const { id, slice, params } = data; + const { resolution } = params; + const vertices = cache[id]; + const box = new THREE.Box2(); + new kiri.topo_slicer(slice.index) + .setFromArray(vertices, slice) + .slice(resolution) + .forEach(rec => { + const { z, index, lines } = rec; + + for (let line of lines) { + const { p1, p2 } = line; + if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } + if (!p2.swapped) { p2.swapXZ(); p2.swapped = true } + } + + raster_slice({ + ...params, + box, + lines, + gridx: index + }); + }); + // only pass back bounds of rasters to be merged + reply({ seq, box }); + }; + + funcs.trace_init = data => { + const { cache } = self; + const { codec } = kiri; + data.cross.clipTo = codec.decode(data.cross.clipTo); + data.cross.clipTab = codec.decode(data.cross.clipTab); + const probe = new Probe(data.probe); + const trace = new Trace(probe, data.trace); + cache.trace = { + probe, + trace, + cross: data.cross + }; + trace.init(data.cross); + }; + + funcs.trace_y = (data, seq) => { + const { cache } = self; + const { trace } = cache.trace; + trace.crossY_sync(data.params, slice => { + slice = kiri.codec.encode(slice); + reply({ seq, slice }); + }); + }; + + funcs.trace_x = (data, seq) => { + const { cache } = self; + const { trace } = cache.trace; + trace.crossX_sync(data.params, slice => { + slice = kiri.codec.encode(slice); + reply({ seq, slice }); + }); + }; + + funcs.trace_cleanup = () => { + delete cache.trace; + }; +}); + + +export { raster_slice, PRO, POLY, RAD2DEG, DEG2RAD, ctab, debug, output, probe, trace, box, params, vertices, range, i, x, z, shards, step, slices, index, slice, complete, promises, concurrent, stepsTaken, pcount, slicesY, slicesX, promise, inc, dec, gridx, gridy, y, lines, points, toolAtZ, sx, tx, ty, tz, rx, ry, toolAtXY, zAtXY, ix, iy, newslice, newtrace, end_poly, log, push_point, newP, lastP, dl, dz, slope, dv, angle, object, poly, zok, checkr, tv, len, l1, tp, j, l2, p1, crossz, spansy, dy, nup, p2, Topo }; diff --git a/src2/kiri-mode/cam/topo4.js b/src2/kiri-mode/cam/topo4.js new file mode 100644 index 00000000..fc4807b0 --- /dev/null +++ b/src2/kiri-mode/cam/topo4.js @@ -0,0 +1,534 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { base } from '../geo/base.js'; +import { line } from '../geo/line.js'; +import { point } from '../geo/point.js'; +import { polygon } from '../geo/polygon.js'; +import { polygons } from '../geo/polygons.js'; +import { broker } from '../moto/broker.js'; +import { slice } from '../kiri/slice.js'; +import { driver } from '../kiri-mode/cam/driver.js'; +import { slicer2 } from '../kiri-mode/cam/slicer2.js'; + +const { base, kiri, moto } = root; +const { driver, newSlice } = kiri; +const { CAM } = driver; +const { polygons, newPoint, newPolygon, sliceConnect } = base; + +const PRO = CAM.process; +const POLY = polygons; +const RAD2DEG = 180 / Math.PI; +const DEG2RAD = Math.PI / 180; + +function scale(fn, factor = 1, base = 0) { + return (value, msg) => { + fn(base + value * factor, msg); + } +} + +class Topo4 { + constructor() { } + + async generate(opt = {}) { + let { state, op, onupdate, ondone } = opt; + let { widget, settings, tabs } = opt.state; + let { controller, process } = settings; + + let axis = op.axis.toLowerCase(), + tool = new CAM.Tool(settings, op.tool), + bounds = widget.getBoundingBox().clone(), + density = parseInt(controller.animesh || 100) * 2500, + { min, max } = bounds, + span = { + x: max.x - min.x, + y: max.y - min.y + }, + contour = { + x: axis === "x", + y: axis === "y" + }, + zMin = min.z + 0.0001, + tolerance = op.tolerance, + resolution = tolerance ? tolerance : 1 / Math.sqrt(density / (span.x * span.y)), + step = this.step = (tool.traceOffset() * 2) * op.step, + angle = this.angle = op.angle || 1; + + if (tool.isTaperMill() && step === 0) { + step = this.step = op.step * tool.unitScale(); + } + + if (tolerance === 0) { + console.log(widget.id, 'topo4 auto tolerance', resolution.round(4)); + } + + this.zBottom = state.zBottom; + this.resolution = resolution; + this.vertices = widget.getGeoVertices({ unroll: true, translate: true }); + this.tabverts = widget.getTabVertices(); + this.tool = tool.generateProfile(resolution).profile; + this.maxo = tool.profileDim.maxo * resolution; + this.diam = tool.fluteDiameter(); + this.zoff = widget.track.top || 0; + this.leave = op.leave || 0; + this.linear = op.linear || false; + this.lineColor = state.settings.controller.dark ? 0xffff00 : 0x555500; + + onupdate(0, "lathe"); + + const range = this.range = { min: Infinity, max: -Infinity }; + const slices = this.sliced = await this.slice(scale(onupdate, 0.25, 0)); + for (let slice of slices) { + range.min = Math.min(range.min, slice.z); + range.max = Math.max(range.max, slice.z); + slice.output() + .setLayer("lathe", { line: 0x888888 }) + .addPolys(slice.topPolys()); + } + + const lathe = await this.lathe(scale(onupdate, 0.75, 0.25)); + + onupdate(1, "lathe"); + ondone(lathe); + // ondone([...slices, ...lathe]); + + return this; + } + + async slice(onupdate) { + const { vertices, resolution, tabverts, zoff } = this; + + const range = this.range = { min: Infinity, max: -Infinity }; + const box = this.box = new THREE.Box2(); + + // swap XZ in shared array + for (let i=0, l=vertices.length; i slice.max) { + slices.push(slice); + slice = { min: z, max: z + step, index }; + } + index++; + } + slices.push(slice); + // console.log({ shards, range, step, slices }); + + if (kiri.minions.running > 1) { + return await this.sliceMinions(onupdate); + } else { + return await this.sliceWorker(onupdate); + } + } + + async sliceWorker(onupdate) { + const { vertices, slices, resolution } = this; + const { codec } = kiri; + + let output = []; + let complete = 0; + for (let slice of slices) { + const recs = new kiri.topo_slicer(slice.index) + .setFromArray(vertices, slice) + .slice(resolution) + .map(rec => { + const slice = kiri.newSlice(rec.z); + for (let line of rec.lines) { + const { p1, p2 } = line; + if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } + if (!p2.swapped) { p2.swapXZ(); p2.swapped = true } + } + slice.index = rec.index; + slice.addTops(sliceConnect(rec.lines)); + + const points = codec.encodePointArray(rec.lines.map(l => [ l.p1, l.p2 ]).flat()); + const shared = new Float32Array(new SharedArrayBuffer(points.length * 4)); + shared.set(points); + slice.shared = shared; + + return slice; + }); + output.appendAll(recs); + onupdate(++complete /slices.length); + } + + return output; + } + + async sliceMinions(onupdate) { + const { codec } = kiri; + const { queue, putCache, clearCache } = this; + const { vertices, slices, resolution } = this; + putCache("vertices", vertices); + + let complete = 0; + let promises = slices.map(slice => { + return queue("topo4_slice", { + resolution, + slice + }).then(data => { + onupdate(++complete / slices.length); + return data; + }); + }); + + // merge boxes for all rasters for contouring clipping + const output = codec.decode(await Promise.all(promises)) + .map(rec => rec.recs) + .flat() + .map(rec => newSlice(rec.z) + .addTops(rec.polys) + .setFields({ shared: rec.shared })) + .sort((a,b) => a.z - b.z); + + clearCache(); + return output; + } + + async lathe(onupdate) { + if (kiri.minions.running > 1) { + return await this.latheMinions(onupdate); + } else { + return await this.latheWorker(onupdate); + } + } + + lathePath(slices, tool) { + const { resolution, step, zBottom, maxo } = this; + + const tlen = tool.length; + const slen = slices.length; + const sinc = Math.max(1, Math.ceil(step / resolution)); + const heights = []; + + // cull slice lines to only the ones in range (5x faster) + const oslices = []; + for (let slice of slices) { + const lines = slice.lines; + const plen = lines.length; + const rec = { z: slice.z, lines: [] }; + for (let i = 0; i < plen; ) { + ++i; // skip x which should match slice.z + let py0 = lines[i++]; + const pz0 = lines[i++]; + ++i; // skip x which should match slice.z + let py1 = lines[i++]; + const pz1 = lines[i++]; + if ((py0 < -maxo && py1 < -maxo) || (py0 > maxo && py1 > maxo)) { + continue; + } + rec.lines.push(0, py0, pz0, 0, py1, pz1); + } + oslices.push(rec); + } + + // iterate over all slices (real x = z) + // find max real z using z ray intersect from tool point to slice lines + offset + let lz = 0; + for (let si = 0; si < slen; si += sinc) { + const rx = oslices[si].z; + let mz = -Infinity; + // iterate over tool offsets + for (let ti = 0; ti < tlen; ) { + // tool offset in grid units from present x (si) + const xo = tool[ti++]; // x grid offset (slice) + const yo = tool[ti++] * resolution; // y grid offset (mult rez to get real y) + const zo = tool[ti++]; // real z delta offset + // get slice index corresponding with offset + const ts = si + xo; + // outside of slice array, skip + if (ts < 0 || ts >= slen - 1) { + continue; + } + const slice = oslices[ts]; + const lines = slice.lines; + const plen = lines.length; + for (let i = 0; i < plen; ) { + ++i; // skip x which should match slice.z + let py0 = lines[i++]; + const pz0 = lines[i++]; + ++i; // skip x which should match slice.z + let py1 = lines[i++]; + const pz1 = lines[i++]; + if ((py0 <= yo && py1 >= yo) || (py1 <= yo && py0 >= yo)) { + const dz = pz1 - pz0; + const dy = Math.abs(py1 - py0); + if (dy === 0) continue; + const fr = Math.abs(yo - py0) / dy; + const lz = pz0 + dz * fr + zo; + // check z height + mz = Math.max(mz, lz); + } + } + if (mz === -Infinity && xo === 0 && yo === 0) { + // tool tip is off the model + continue; + } + } + if (mz === -Infinity) { + mz = zBottom; // Math.min(0, lz) + } else if (zBottom && mz < zBottom) { + mz = zBottom; + } + heights.push(rx, 0, lz = mz); + } + + return heights; + } + + async latheWorker(onupdate) { + const { sliced, tool, zoff, leave } = this; + + const rota = this.angle * DEG2RAD; + const steps = (Math.PI * 2) / rota; + const axis = new THREE.Vector3(1, 0, 0); + const mrot = new THREE.Matrix4().makeRotationAxis(axis, rota); + const slices = sliced.map(s => { return { z: s.z, lines: s.shared } }); + const paths = []; + const recs = []; + + let angle = 0; + let count = 0; + // for each step angle, find Z spine heights, produce record + while (count++ < steps) { + const heights = this.lathePath(slices, tool, paths); + recs.push({ angle, heights, degrees: angle * RAD2DEG }); + angle -= rota; + for (let lines of slices.map(s => s.lines)) { + rotatePoints(lines, mrot); + } + onupdate(count / steps); + } + + count = recs[0].heights.length / 3; + while (count-- > 0) { + let slice = newSlice(count); + slice.camLines = [ newPolygon().setOpen() ]; + paths.push(slice); + } + + for (let rec of recs) { + const { degrees, heights } = rec; + [...heights].group(3).forEach((a,i) => { + // progress each path 360 degrees to prevent A rolling backwards + paths[i].camLines[0].push( newPoint(a[0], a[1], a[2] + leave).setA(degrees + i * -360) ); + }); + } + + for (let slice of paths) { + const poly = slice.camLines[0]; + if (!poly.length) { + console.log('empty', slice); + continue; + } + // repeat first point 360 degrees progressed + const repeat = poly.points[0]; + slice.camLines[0].push(repeat.clone().setA(repeat.a - 360)); + slice.output() + .setLayer("lathe", { line: this.lineColor }) + .addPoly(poly.clone().applyRotations().move({ z: -zoff, x:0, y:0 })); + } + + // console.log({ tool, slices, paths }); + + return paths; + } + + async latheMinions(onupdate) { + const { sliced, tool, zoff, leave, maxo, zBottom, step, resolution, linear } = this; + const { putCache, clearCache, queue } = this; + + const rota = this.angle * DEG2RAD; + const steps = (Math.PI * 2) / rota; + const slices = sliced.map(s => { return { z: s.z, lines: s.shared } }); + const paths = []; + const recs = []; + + putCache("lathe", { + maxo, + tool, + step, + slices, + zBottom, + resolution, + }); + + let done = 0; + let tangle = 0; + let count = 0; + let promises = []; + // for each step angle, find Z spine heights, produce record + while (count++ < steps) { + const angle = tangle; + let p = new Promise(resolve => { + queue("topo4_lathe", { angle }).then(data => { + // console.log({ angle, data }); + recs.push({ angle, heights: data.heights, degrees: angle * RAD2DEG }); + onupdate(++done / steps); + resolve(); + }); + }); + // await p; + promises.push(p); + tangle -= rota; + } + + await Promise.all(promises); + recs.sort((a, b) => { return b.angle - a.angle }); + + // let linear = true; + + count = linear ? recs.length : recs[0].heights.length / 3; + while (count-- > 0) { + let slice = newSlice(count); + slice.camLines = [ newPolygon().setOpen() ]; + paths.push(slice); + } + + if (linear) { + recs.forEach((rec,i) => { + const { degrees, heights } = rec; + [...heights].group(3).forEach((a) => { + paths[i].camLines[0].push( newPoint(a[0], a[1], a[2] + leave).setA(degrees) ); + }); + if (i % 2 === 1) { + paths[i].camLines[0].reverse(); + } + }); + } else { + for (let rec of recs) { + const { degrees, heights } = rec; + [...heights].group(3).forEach((a,i) => { + // progress each path 360 degrees to prevent A rolling backwards + paths[i].camLines[0].push( newPoint(a[0], a[1], a[2] + leave).setA(degrees + i * -360) ); + }); + } + } + + for (let slice of paths) { + const poly = slice.camLines[0]; + if (!poly.length) { + console.log('empty', slice); + continue; + } + if (!linear) { + // repeat first point 360 degrees progressed + const repeat = poly.points[0]; + slice.camLines[0].push(repeat.clone().setA(repeat.a - 360)); + } + slice.output() + .setLayer("lathe", { line: this.lineColor }) + .addPoly(poly.clone().applyRotations().move({ z: -zoff, x:0, y:0 })); + } + + // console.log({ tool, slices, paths }); + clearCache(); + + return paths; + } + + putCache(key, data) { + kiri.worker.putCache({ key, data }, { done: data => { } }); + } + + clearCache() { + kiri.worker.clearCache({}, { done: data => { } }); + } + + queue(cmd, params) { + return new Promise(resolve => { + kiri.minions.queue({ cmd, ...params }, resolve); + }); + } +} + +function rotatePoints(lines, rot) { + new THREE.BufferAttribute(lines, 3).applyMatrix4(rot); +} + +CAM.Topo4 = function(opt) { + return new Topo4().generate(opt); +}; + +moto.broker.subscribe("minion.started", msg => { + const { funcs, cache, reply, log } = msg; + const { codec } = kiri; + + funcs.topo4_slice = (data, seq) => { + const { slice, resolution } = data; + const vertices = cache.vertices; + const recs = new kiri.topo_slicer(slice.index) + .setFromArray(vertices, slice) + .slice(resolution) + .map(rec => { + const { z, index, lines } = rec; + + for (let line of lines) { + const { p1, p2 } = line; + if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } + if (!p2.swapped) { p2.swapXZ(); p2.swapped = true } + } + + const points = codec.encodePointArray(lines.map(l => [ l.p1, l.p2 ]).flat()); + const shared = new Float32Array(new SharedArrayBuffer(points.length * 4)); + shared.set(points); + + return { + z, index, shared, + polys: codec.encode(sliceConnect(lines)), + }; + }); + // only pass back bounds of rasters to be merged + reply({ seq, recs }); + }; + + funcs.topo4_lathe = (data, seq) => { + const { angle } = data; + const { slices, tool } = cache.lathe; + + const axis = new THREE.Vector3(1, 0, 0); + const mrot = new THREE.Matrix4().makeRotationAxis(axis, -angle); + const stmp = slices.map(s => { + const lines = s.lines.slice(); + rotatePoints(lines, mrot); + return { z: s.z, lines } + }); + + const topo4 = Object.assign(new Topo4(), cache.lathe); + const heights = topo4.lathePath(stmp, tool); + + reply({ seq, heights }); + } + +}); + + +export { scale, rotatePoints, PRO, POLY, RAD2DEG, DEG2RAD, axis, range, slices, lathe, box, i, x, z, shards, step, index, slice, output, complete, recs, points, shared, promises, tlen, slen, sinc, heights, oslices, lines, plen, rec, py0, pz0, py1, pz1, lz, si, rx, mz, ti, xo, yo, zo, ts, dz, dy, fr, rota, steps, mrot, paths, angle, count, poly, repeat, done, tangle, p, linear, vertices, stmp, topo4, Topo4 }; diff --git a/src2/kiri-mode/drag/driver.js b/src2/kiri-mode/drag/driver.js new file mode 100644 index 00000000..bce92d28 --- /dev/null +++ b/src2/kiri-mode/drag/driver.js @@ -0,0 +1,11 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { driver } from '../kiri-mode/laser/driver.js'; + + +const DRIVERS = root.kiri.driver; +const { LASER } = DRIVERS; +const { TYPE } = LASER; +DRIVERS.DRAG = Object.assign({}, LASER, { name: "DragKnife", type: TYPE.DRAG }); + + +export { DRIVERS }; diff --git a/src2/kiri-mode/fdm/client.js b/src2/kiri-mode/fdm/client.js new file mode 100644 index 00000000..cc2f4102 --- /dev/null +++ b/src2/kiri-mode/fdm/client.js @@ -0,0 +1,627 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from '../../kiri/api.js'; +import { conf } from '../../kiri/conf.js'; +import { addbox, delbox } from '../../kiri/boxes.js'; +import { Layers } from '../../kiri/layers.js'; +import { Stack } from '../../kiri/stack.js'; +import { Widget, newWidget } from '../../kiri/widget.js'; +import { space } from '../../moto/space.js'; + +const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE; +const { VIEWS } = api.const; + +const LANG = api.language.current; + +let lastBox, lastMode, lastView, lastPillar, fromPillar, + addingSupports = false, + isFdmMode = false, + nextID = Date.now(), + p1, p2, iw, + alert = [], + func = {}; + +export function init() { + const { ui } = api; + const proc_keys = Object.keys(conf.defaults.fdm.p); + const rangeVars = { + // slice + // "sliceHeight": LANG.sl_lahi_s, + "sliceShells": LANG.sl_shel_s, + "sliceFillType": LANG.fi_type, + "sliceFillWidth": LANG.fi_wdth_s, + "sliceFillSparse": LANG.fi_pcnt_s, + "sliceFillRate": LANG.ou_feed_s, + "sliceFillGrow": LANG.fi_grow_s, + "sliceSolidRate": LANG.ou_fini_s, + "sliceSolidMinArea": LANG.ad_msol_s, + "sliceTopLayers": LANG.sl_ltop_s, + "sliceBottomLayers": LANG.sl_lbot_s, + "firstLayerRate": LANG.fl_rate_s, + "firstLayerPrintMult": LANG.fl_mult_s, + "firstLayerFanSpeed": LANG.ou_fans_s, + // prepare + "sliceShellOrder": LANG.sl_ordr_s, + "sliceFillOverlap": LANG.fi_over_s, + "outputFeedrate": LANG.ou_feed_s, + "outputFinishrate": LANG.ou_fini_s, + "outputShellMult": LANG.ou_shml_s, + "outputFillMult": LANG.ou_flml_s, + "outputSparseMult": LANG.ou_spml_s, + "outputRetractWipe": LANG.ad_wpln_s, + "outputAlternating": LANG.ad_altr_s, + "outputShortPoly": LANG.ad_spol_s, + "outputMinSpeed": LANG.ad_mins_s, + "outputCoastDist": LANG.ad_scst_s, + "outputLoops": LANG.ag_loop_s, + "sliceSupportDensity": LANG.sp_dens_s, + "sliceSupportOffset": LANG.sp_offs_s, + "sliceLayerStart": LANG.sl_strt_s, + "firstLayerBrim": LANG.fl_brim_s, + "firstLayerBrimIn": LANG.fl_brin_s, + "firstLayerBrimTrig": LANG.fl_brmn_s, + "firstLayerBrimGap": LANG.fl_brgp_s, + // export + "zHopDistance": LANG.ad_zhop_s, + "arcTolerance": LANG.ad_zhop_s, + "antiBacklash": LANG.ad_abkl_s, + "outputTemp": LANG.ou_nozl_s, + "outputBedTemp": LANG.ou_bedd_s, + "outputFanSpeed": LANG.ou_fans_s, + "outputRetractDist": LANG.ad_rdst_s, + "outputRetractSpeed": LANG.ad_rrat_s, + "outputRetractDwell": LANG.ad_rdwl_s, + "outputLayerRetract": LANG.ad_lret_s + }; + + for (let key of Object.keys(rangeVars)) { + if (ui[key]) { + ui[key].range = true; + } + } + + function filterSynth() { + api.widgets.filter((widget) => { + if (widget.track.synth) { + space.world.remove(widget.mesh); + Widget.Groups.remove(widget); + } + return !widget.track.synth + }); + } + + function clearRanges() { + api.conf.get().process.ranges = []; + ui.fdmRanges.innerHTML = ''; + } + + // re-render ranges menu + function updateRanges(ranges = []) { + let html = []; + let bind = []; + let now = Date.now(); + let sorted = ranges.sort((a,b) => b.lo - a.lo); + for (let range of sorted) { + let id = (now++).toString(36); + let rows = Object.entries(range.fields).map(a => `
`).join(''); + let hover = `
${rows}
`; + let info = ``; + html.appendAll([ + `
${hover}${info}
` + ]); + bind.push({id, range}); + } + ui.fdmRanges.innerHTML = html.join(''); + for (let rec of bind) { + $(`sel_${rec.id}`).onclick = () => { + api.show.layer(rec.range.hi, rec.range.lo); + }; + $(`del_${rec.id}`).onclick = () => { + let io = ranges.indexOf(rec.range); + ranges.splice(io,1); + updateRanges(ranges); + }; + } + } + + api.event.on("function.animate", (mode) => { + if (!(isFdmMode && lastView === VIEWS.PREVIEW)) { + return; + } + let slider = $('top-slider'); + slider.style.display = 'flex'; + slider.oninput = slider.onchange = (ev) => { + api.const.STACKS.setFraction(parseInt(ev.target.value)/1000); + api.space.update(); + }; + $('anim-slider').value = 500; + api.const.STACKS.setFraction(0.5); + }); + + api.event.on("mode.set", mode => { + isFdmMode = mode === 'FDM'; + lastMode = mode; + // hide/show fdm mode elements + for (let el of [...document.getElementsByClassName('mode-fdm')]) { + api.uc.setClass(el, 'hide', !isFdmMode); + } + updateSupportVisiblity(); + }); + + api.event.on("view.set", view => { + $('top-slider').style.display = 'none'; + lastView = view; + updateSupportVisiblity(); + filterSynth(); + func.sdone(); + // let ranges = api.conf.get().process.ranges; + if (isFdmMode) { + if (lastView === VIEWS.SLICE) { + for (let key of proc_keys) { + if (ui[key] && !ui[key].range) { + ui[key].disabled = true; + } + } + } else { + for (let key of proc_keys) { + if (ui[key]) ui[key].disabled = false; + } + } + } + }); + + api.event.on("range.updates", updateRanges); + + api.event.on("settings.load", (settings) => { + if (settings.mode !== 'FDM') return; + restoreSupports(api.widgets.all()); + updateRanges(settings.process.ranges); + }); + + api.event.on("settings.saved", (settings) => { + updateRanges(settings.process.ranges); + }); + + api.event.on("button.click", target => { + switch (target) { + case api.ui.ssaGen: return func.sgen(); + case api.ui.ssmAdd: return func.sadd(); + case api.ui.ssmDun: return func.sdone(); + case api.ui.ssmClr: return func.sclear(); + } + }); + + api.event.on("fdm.supports.detect", func.sgen = () => { + let alerts = []; + let { process, device } = api.conf.get(); + if (!device.bedBelt) { + if (process.sliceSupportAngle < 10) { + alerts.push(api.show.alert("angles below 10 degrees may fail")); + } + } + alerts.push(api.show.alert("analyzing part(s)...", 1000)); + FDM.support_generate(array => { + func.sclear(); + api.hide.alert(undefined,alerts); + for (let rec of array) { + let { widget, supports } = rec; + let wa = api.widgets.annotate(widget.id); + let ws = wa.support || []; + for (let support of supports) { + let { from, to, mid } = support; + let dw = api.conf.get().process.sliceSupportSize / 2; + let dh = from.z - to.z; + let rec = { + x: mid.x, + y: mid.y, + z: mid.z, + dw, + dh, + id: Math.random() * 0xffffffffff + }; + addWidgetSupport(widget, rec); + ws.push(Object.clone(rec)); + } + wa.support = ws; + } + api.event.emit("fdm.supports.detected"); + }); + }); + + api.event.on("fdm.supports.add", func.sadd = () => { + alert = api.show.alert("[esc] key cancels support editing"); + api.feature.hover = addingSupports = true; + }); + + api.event.on("fdm.supports.done", func.sdone = () => { + delbox('intZ'); + delbox('intW'); + delbox('supp'); + api.hide.alert(alert); + api.feature.hover = addingSupports = false; + fromPillar = undefined; + }); + + api.event.on("fdm.supports.clear", func.sclear = () => { + func.sdone(); + if (clearAllWidgetSupports()) { + api.conf.save(); + } + }); + + api.event.on("slice.begin", () => { + if (!isFdmMode) { + return; + } + func.sdone(); + updateSupportVisiblity(); + // synth support widget for each widget group + // iow, create support meshes based on support widget annotation + for (let group of Widget.Groups.list()) { + let merge = group.filter(w => w.sups).map(w => Object.values(w.sups)).flat(); + if (!merge.length) { + continue; + } + let boxen = merge.map(m => { + let geo = m.box.geometry.clone(); + if (geo.index) geo = geo.toNonIndexed(); + return geo.translate(m.x, m.y, m.z); + }); + for (let box of boxen) { + // eliminate / normalize uv to allow other widget merge + box.setAttribute('uv',new BufferAttribute(new Float32Array(0), 3)); + } + let bbg = BufferGeometryUtils.mergeGeometries(boxen); + let sw = newWidget(null, group); + let fwp = group[0].track.pos; + sw.loadGeometry(bbg); + sw._move(fwp.x, fwp.y, fwp.z); + api.widgets.add(sw); + sw.track.synth = true; + sw.track.support = true; + space.world.add(sw.mesh); + } + }); + + api.event.on("slice.end", () => { + if (!isFdmMode) { + return; + } + }); + + api.event.on("key.esc", () => { + if (isFdmMode) { + func.sdone() + } + }); + + api.event.on("selection.scale", () => { + if (isFdmMode) { + clearRanges(); + func.sclear(); + } + }); + + api.event.on("widget.delete", widget => { + if (isFdmMode) { + clearRanges(); + } + }); + + api.event.on("widget.duplicate", (widget, oldwidget) => { + if (!isFdmMode) { + return; + } + let ann = api.widgets.annotate(widget.id); + if (ann.support) { + for (let supp of Object.values(ann.support)) { + supp.id = Math.abs(Math.random() * 0xffffffffffff); + } + restoreSupports([widget]); + } + }); + + api.event.on("widget.mirror", widget => { + if (!isFdmMode) { + return; + } + let ann = api.widgets.annotate(widget.id); + let sups = ann.support || []; + sups.forEach(sup => { + let wsup = widget.sups[sup.id]; + wsup.box.position.x = wsup.x = sup.x = -sup.x; + }); + }); + + api.event.on("widget.rotate", rot => { + if (!isFdmMode) { + return; + } + let {widget, x, y, z} = rot; + if (x || y) { + clearWidgetSupports(widget); + } else { + let ann = api.widgets.annotate(widget.id); + let sups = ann.support || []; + sups.forEach(sup => { + let wsup = widget.sups[sup.id]; + let vc = new Vector3(sup.x, sup.y, sup.z); + let m4 = new Matrix4(); + m4 = m4.makeRotationFromEuler(new Euler(x || 0, y || 0, z || 0)); + vc.applyMatrix4(m4); + wsup.box.position.x = wsup.x = sup.x = vc.x; + wsup.box.position.y = wsup.y = sup.y = vc.y; + wsup.box.position.z = wsup.z = sup.z = vc.z; + }); + } + }); + + api.event.on("mouse.hover.up", func.hoverup = (on = {}) => { + let { event } = on; + if (!isFdmMode) { + return; + } + if (!addingSupports) { + return; + } + delbox('supp'); + if (lastPillar) { + removeWidgetSupport(lastPillar.widget, lastPillar); + return; + } + if (!(iw && lastBox)) return; + let { point, dim } = lastBox; + p1.y = Math.max(0, p1.y); + p2.y = Math.max(0, p2.y); + let rz = dim.rz; + let dh = dim.z; + let dw = api.conf.get().process.sliceSupportSize / 2; + let ip = iw.track.pos; + let wa = api.widgets.annotate(iw.id); + let ws = (wa.support = wa.support || []); + let id = ++nextID; + let rec = {x: point.x - ip.x, y: -point.z - ip.y, z: point.y, rz, dw, dh, id}; + if (fromPillar && event && event.shiftKey) { + let targets = api.widgets.meshes().append(space.internals().platform); + let from = fromPillar, + d = { + x: rec.x - from.x, + y: rec.y - from.y, + z: rec.z - from.z + }; + let dist = Math.sqrt(d.x*d.x + d.y*d.y + d.z*d.z); + let lerp = util.lerp(0,dist,dw).map(v => v/dist); + let angle = rec.rz = Math.atan2(d.y,d.x); + let fromrec = ws.filter(r => r.id === from.id)[0]; + if (fromrec) { + from.rz = fromrec.rz = angle; + removeWidgetSupport(iw, fromPillar); + ws.push(Object.clone(fromrec)); + addWidgetSupport(iw, fromrec); + } + lerp.pop(); + let seed = Math.random(); + for (let pct of lerp) { + let point = new Vector3(from.x + d.x * pct, from.z + d.z * pct, from.y + d.y * pct); + addSupportAtPoint(targets, point, ip, angle, seed++); + } + } + ws.push(Object.clone(rec)); + fromPillar = addWidgetSupport(iw, rec); + api.conf.save(); + }); + + function addSupportAtPoint(targets, point, ip, angle, id) { + let up = new Vector3(0,1,0) + let dn = new Vector3(0,-1,0) + let rp = new Vector3(point.x + ip.x, point.y, -point.z - ip.y); + let iup = new Raycaster(rp, up) + .intersectObjects(targets, false) + .filter(t => !t.object.pillar); + if (iup.length && iup.length % 2 === 0) { + let idn = new Raycaster(rp, dn) + .intersectObjects(targets, false) + .filter(t => !t.object.pillar); + if (idn.length && idn.length % 2 === 0) { + iw = iup[0].object.widget || iw; + let wa = api.widgets.annotate(iw.id); + let ws = (wa.support = wa.support || []); + let phi = iup[0].point; + let plo = idn[0].point; + let ply = Math.max(0, plo.y); + let mp = (phi.y + ply) / 2; + let dy = Math.abs(phi.y - ply); + let dw = api.conf.get().process.sliceSupportSize / 2; + let rec = { rz:angle, x:point.x, y:point.z, z:mp, dw, dh:dy, id }; + ws.push(Object.clone(rec)); + fromPillar = addWidgetSupport(iw, rec); + } + } + } + + api.event.on("mouse.hover", data => { + if (!isFdmMode) { + return; + } + if (!addingSupports) { + return; + } + delbox('supp'); + const { int, type, point, event } = data; + const pillar = int ? int.object.pillar : undefined; + if (lastPillar) { + lastPillar.box.material.color.r = 0; + lastPillar = null; + } + if (pillar) { + pillar.box.material.color.r = 0.5; + lastPillar = pillar; + if (event && (event.metaKey || event.ctrlKey)) { + return func.hoverup(); + } + return; + } + if (int && type === 'widget') { + iw = int.object.widget || iw; + } else { + iw = null; + } + p1 = point; + let dir = new Vector3(0,1,0) + let ray = new Raycaster(point, dir); + let rz = int && int.face ? Math.atan2(int.face.normal.y, int.face.normal.x) : 0; + // when on object, project down on downward faces + if (int && int.face && int.face.normal.z < -0.1) { + dir.y = -1; + } + let targets = api.widgets.meshes().append(space.internals().platform); + let i2 = ray.intersectObjects(targets, false) + .filter(t => !t.object.pillar) // eliminate other pillars + .filter(i => i.distance > 0.1); // false matches close to origin of ray + if (i2.length && i2.length % 2 === 0) { + p2 = i2[0].point; + iw = i2[0].object.widget || iw; + let p1y = Math.max(0, p1.y); + let p2y = Math.max(0, p2.y); + let hy = (p1y + p2y) / 2; + let dy = Math.abs(p1y - p2y); + let dw = api.conf.get().process.sliceSupportSize / 2; + addbox({x:p1.x, y:hy, z:p1.z}, 0x0000dd, 'supp', { + x:dw, y:dw, z:dy, rz + }); + } else { + lastBox = null; + } + if (event && event.altKey) { + return func.hoverup(); + } + }); + + api.event.on("export.debug", msg => { + if (msg.arcQ && msg.arcQ.length > 2) { + if (xpdebug) { + xpdebug.destroy(); + xpdebug = undefined; + } + let poly = base.newPolygon().setOpen(); + for (let rec of msg.arcQ) { + rec.z += 1; + poly.addObj(rec); + } + + let layers = new Layers(); + let layer = layers.setLayer("arcq", { line: 0xff00ff, face: 0xff00ff, opacity: 1 }); + layer.addPoly(poly, {thin:true}); + + layer = layers.setLayer("arcc", { line: 0xff0000, face: 0xff0000, opacity: 1 }); + for (let center of msg.arcQ.center) { + center.z += 0.5; + layer.addPoly(base.newPolygon().centerCircle( + center, center.r, 20 + )); + } + + xpdebug = new Stack(space.world, false); + xpdebug.addLayers(layers); + xpdebug.setVisible(0,Infinity); + xpdebug.show(); + + space.refresh(); + + console.log(msg, poly); + } + }); +} + +let xpdebug; +let supsave = {}; +let suptimer; + +function scheduleSupportSave(widget) { + clearTimeout(suptimer); + supsave[widget.id] = widget; + suptimer = setTimeout(() => { + for (let w of Object.values(supsave)) { + w.saveState(); + } + }, 50); +} + +export function restoreSupports(widgets) { + widgets.forEach(widget => { + const supports = api.widgets.annotate(widget.id).support || []; + supports.forEach(pos => { + addWidgetSupport(widget, pos); + }); + }); +} + +function addWidgetSupport(widget, pos) { + const { x, y, z, rz, dw, dh, id } = pos; + const sups = widget.sups = (widget.sups || {}); + // prevent duplicate restore from repeated settings load calls + if (!sups[id]) { + pos.box = addbox( + { x, y, z }, 0x0000dd, id, + { x:dw, y:dw, z:dh, rz }, { group: widget.mesh } + ); + pos.box.pillar = Object.assign({widget}, pos); + sups[id] = pos; + widget.adds.push(pos.box); + scheduleSupportSave(widget); + } + return sups[id]; +} + +function removeWidgetSupport(widget, rec) { + const { box, id } = rec; + widget.adds.remove(box); + widget.mesh.remove(box); + delete widget.sups[id]; + let sa = api.widgets.annotate(widget.id).support; + let ix = 0; + sa.forEach((rec,i) => { + if (rec.id === id) { + ix = i; + } + }); + sa.splice(ix,1); + api.conf.save(); + scheduleSupportSave(widget); + fromPillar = undefined; +} + +function updateSupportVisiblity() { + const visible = (lastMode === 'FDM' && lastView === VIEWS.ARRANGE); + for (let w of api.widgets.all()) { + setSupportVisiblity(w, visible); + } +} + +function setSupportVisiblity(widget, bool) { + Object.values(widget.sups || {}).forEach(support => { + support.box.visible = bool; + }); +} + +function clearAllWidgetSupports() { + let cleared = 0; + api.widgets.all().forEach(widget => { + cleared += clearWidgetSupports(widget); + }); + return cleared; +} + +function clearWidgetSupports(widget) { + let cleared = 0; + Object.values(widget.sups || {}).forEach(support => { + widget.adds.remove(support.box); + widget.mesh.remove(support.box); + cleared++; + }); + widget.sups = {}; + delete api.widgets.annotate(widget.id).support; + scheduleSupportSave(widget); + return cleared; +} diff --git a/src2/kiri-mode/fdm/driver.js b/src2/kiri-mode/fdm/driver.js new file mode 100644 index 00000000..83967810 --- /dev/null +++ b/src2/kiri-mode/fdm/driver.js @@ -0,0 +1,83 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { base } from '../../../geo/base.js'; +import { kiri } from '../../kiri.js'; +import { driver } from '../../kiri.js'; +import { util } from '../../../geo/base.js'; + +const { base, kiri } = root; +const { driver } = kiri; +const { util } = base; +const FDM = driver.FDM = { getRangeParameters, extrudePerMM, extrudeMM }; + +// shared by client and worker contexts +function getRangeParameters(process, index) { + if (index === undefined || index === null || index < 0) { + return process; + } + let ranges = process.ranges; + if (!(ranges && ranges.length)) { + return process; + } + let params = Object.clone(process); + for (let range of ranges) { + if (index >= range.lo && index <= range.hi) { + for (let [key, value] of Object.entries(range.fields)) { + params[key] = value; + params._range = true; + } + } + } + return params; +} + +// noz = nozzle diameter +// fil = filament diameter +// slice = slice height +function extrudePerMM(noz, fil, slice) { + return ((Math.PI * util.sqr(noz / 2)) / (Math.PI * util.sqr(fil / 2))) * (slice / noz); +}; + +// dist = distance between extrusion points +// perMM = amount extruded per MM (from extrudePerMM) +// factor = scaling factor (usually 1.0) +function extrudeMM(dist, perMM, factor) { + return dist * perMM * factor; +} + +// defer loading until client and worker exist +kiri.load(api => { + + const { client, worker } = kiri; + + if (client) { + FDM.support_generate = function(ondone) { + client.clear(); + client.sync(); + const settings = api.conf.get(); + const widgets = api.widgets.map(); + client.send("fdm_support_generate", { settings }, (gen) => { + if (gen && gen.error) { + api.show.alert('support generation canceled'); + return ondone([]); + } + for (let g of gen) { + g.widget = widgets[g.id]; + } + ondone(gen); + }); + }; + } + + if (worker) { + worker.fdm_support_generate = function(data, send) { + const { settings } = data; + const widgets = Object.values(worker.cache); + const fresh = widgets.filter(widget => FDM.supports(settings, widget)); + send.done(kiri.codec.encode(fresh.map(widget => { return { + id: widget.id, + supports: widget.supports, + } } ))); + }; + } + +}); diff --git a/src2/kiri-mode/fdm/export.js b/src2/kiri-mode/fdm/export.js new file mode 100644 index 00000000..b6a2c5d4 --- /dev/null +++ b/src2/kiri-mode/fdm/export.js @@ -0,0 +1,979 @@ +import { base } from '../geo/base.js'; +import { utils } from '../kiri/utils.js'; +import { driver } from '../kiri-mode/fdm/driver.js'; + + +const const { base, kiri } = root;;const const { util, config } = base;;const const { FDM } = kiri.driver;;const const { getRangeParameters } = FDM;;const debug = false; + +FDM.export = function(print, online, ondone, ondebug) const { const { widgets, settings, belty, tools, firstTool } = print;;const const { bounds, controller, device, process, filter, mode } = settings;;const const { extruders, fwRetract } = device;;const const { bedWidth, bedDepth, bedRound, bedBelt, maxHeight } = device;;const const { gcodeFan, gcodeLayer, gcodeTrack, gcodePause, gcodeFeature, gcodeChange } = device;;let model_labels = []; + for (let widget of widgets) { + model_labels.push(widget.track.grid_id); + } + + let layers = print.output, + extras = device.extras || {}, + isBambu = extras.bbl, + const { extrudeAbs } = device;const { exportThumb } = controller;extused = Object.keys(print.extruders).map(v => parseInt(v)), + timeFactor = (device.gcodeTime || 1) * 1.5, + decimals = config.gcode_decimals || 4, + zMoveMax = device.deviceZMax || 0, + isBelt = bedBelt, + bedType = isBelt ? "belt" : "fixed", + time = 0, + layer = 0, + layerno = 0, + pause = [], + output = [], + outputLength = 0, // absolute extruder position + lastProgress = 0, + progress = 0, + distance = 0, + emitted = 0, + retracted = 0, + axis = { X:' X', Y:' Y', Z:' Z', E:' E'}, + pos = { x:0, y:0, z:0, f:0 }, + lout = { x:0, y:0, z:0 }, + last = null, + zpos = 0, + bmax = 0, + blastz = 0, + belt_add_y = (process.firstLayerYOffset || 0) - (belty || 0), + oloops = process.outputLoops || 0, + zhop = process.zHopDistance || 0, // range + lineWidth = process.sliceLineWidth || 0, + seekMMM = process.outputSeekrate * 60, + retDist = process.outputRetractDist || 0, // range + retSpeed = process.outputRetractSpeed * 60 || 1, // range + retDwell = process.outputRetractDwell || 0, // range + timeDwell = retDwell / 1000, + arcDist = isBelt ? 0 : (process.arcTolerance || 0), + arcMin = 1, + arcRes = 20, + arcDev = 0.5, + arcMax = 40, + originCenter = device.originCenter || bedRound, + offset = originCenter ? { + x: 0, + y: 0, + z: 0 + } : { + x: bedWidth/2, + y: isBelt ? 0 : bedDepth/2, + z: 0 + }, + tool = firstTool || 0, + extruder = extruders[tool], + offset_x = extruder.extOffsetX, + offset_y = extruder.extOffsetY, + extrudeSet = false, + nozzleTemp = process.firstLayerNozzleTemp || process.outputTemp, + bedTemp = process.firstLayerBedTemp || process.outputBedTemp, + fanSpeedSave = undefined, + fanSpeedBase = 0, + fanSpeed = 0, + lastType = undefined, + lastNozzleTemp = nozzleTemp, + lastBedTemp = bedTemp, + lastFanSpeed = fanSpeed, + subst = { + minx: bounds.min.x + offset.x, + maxx: bounds.max.x + offset.x, + miny: bounds.min.y + offset.y, + maxy: bounds.max.y + offset.y, + travel_speed: seekMMM, + retract_speed: retSpeed, + retract_distance: retDist, + temp: nozzleTemp, + temp_bed: bedTemp, + bed_temp: bedTemp, + fan_speed_base: fanSpeedBase, + fan_speed: fanSpeed, + speed: fanSpeed, // legacy + top: offset.y ? bedDepth : bedDepth/2, + left: offset.x ? 0 : -bedWidth/2, + right: offset.x ? bedWidth : bedWidth/2, + bottom: offset.y ? 0 : -bedDepth/2, + z_max: maxHeight, + layers: layers.length, + progress: 0, + nozzle: tool, + tool: tool, + model_labels: model_labels.sort().join(',') + }, + pidx, path, out, speedMMM, emitMM, emitPerMM, lastp, laste, dist, + lines = 0, + bytes = 0, + bcos = Math.cos(Math.PI / 4), + icos = 1 / bcos, + inloops = 0, + arcQ = [], + minz = { x: Infinity, y: Infinity, z: Infinity }, + // lenghts of each filament (by nozzle) consumed + segments = [], + extrudeMM = FDM.extrudeMM, + extrudePerMM = FDM.extrudePerMM; + + let tools_used = Object.keys(tools); + for (let tool of tools_used) { + subst[`tool_used_${tool}`] = true; + } + subst.tool_count = tools_used.length; + + // encodes an array offset as a single "on" bit in an + // 8 byte array, then converts the array to base64 which + // is what Bambu's M624 uses to flag an object as currently + // being printed. the array is presented at the top of the + // gcode as a comment: "; model label id: {model_labels}" + function encodeBitOffset(index) { + let bytes = new Uint8Array(8); + let byteIndex = Math.floor(index / 8); + let bitPosition = index % 8; + bytes[byteIndex] |= (1 << bitPosition); + return btoa(String.fromCharCode(...bytes)); + } + + function setTempFanSpeed(tempSpeed) { + if (tempSpeed > 0) { + fanSpeedSave = fanSpeedSave >= 0 ? fanSpeedSave : fanSpeed; + fanSpeed = tempSpeed; + subst.fan_speed = fanSpeed; + subst.fan_speed_base = tempSpeed; + } else { + fanSpeed = fanSpeedSave >= 0 ? fanSpeedSave : fanSpeed + fanSpeedSave = undefined; + subst.fan_speed = fanSpeed; + subst.fan_speed_base = fanSpeedBase; + } + } + + // smallish band-aid. refactor above to remove redundancy + function updateParams(layer, params) { + // params = getRangeParameters(process, layer); + zhop = params.zHopDistance || 0; // range + retDist = params.outputRetractDist || 0; // range + retSpeed = params.outputRetractSpeed * 60 || 1; // range + retDwell = params.outputRetractDwell || 0; // range + timeDwell = retDwell / 1000; + nozzleTemp = layer === 0 ? + params.firstLayerNozzleTemp || params.outputTemp : + params.outputTemp || params.firstLayerNozzleTemp; + bedTemp = layer === 0 ? + params.firstLayerBedTemp || params.outputBedTemp : + params.outputBedTemp || params.firstLayerBedTemp; + fanSpeedBase = params.firstLayerFanSpeed || 0; + fanSpeed = layer >= (params.outputFanLayer ?? 1) ? + params.outputFanSpeed || 0 : fanSpeedBase; + Object.assign(subst, { + temp_bed: bedTemp, + bed_temp: bedTemp, + fan_speed: fanSpeed, + fan_speed_base: fanSpeedBase, + speed: fanSpeed, // legacy + retract_speed: retSpeed, + retract_distance: retDist, + temp: params.outputTemp, // range + temp_bed: params.outputBedTemp, // range + bed_temp: params.outputBedTemp, // range + }); + } + + let rloops = []; + if (oloops > 0) { + // if oloops set, loop entire part + rloops.push({ + start: layers[0].slice.index, + end: Infinity, + iter: oloops - 1 + }); + } else if (oloops < 0) { + // if oloops negative, loop entire part to infinity + rloops.push({ + start: layers[0].slice.index, + end: Infinity, + iter: 0 + }); + } + if (process.ranges) { + // collect loops from ranges and synth range array + for (let range of process.ranges) { + if (range.fields.outputLoops) { + rloops.push({ + start: range.lo, + end: range.hi, + iter: range.fields.outputLoops - 1 + }); + } + } + } + let loops = isBelt && rloops.length ? rloops : undefined; + + function append(line) { + if (line) { + lines++; + bytes += line.length; + output.append(line); + } + // batch gcode output to free memory in worker + // consider alternate transfer schemes like indexeddb + // since this transfers the burden to the main thread + if (!line || output.length > 1000) { + online(output.join("\n")); + output = []; + } + }; + + function appendSubPad(line, pad) { + appendSub(line, true); + } + + let subon = true; + let ifhit = false; + + // eval() based var substitution (from print) + // with logic flow IF / ELIF / ELSE / END + // IF / IF is valid but will not nest + function appendSub(line, pad) { + if (line.indexOf(';; DEFINE ') === 0) { + // ignore var declarations + } else if (line.indexOf(';; IF ') === 0) { + line = line.substring(6).trim(); + let evil = print.constReplace(line, subst, 0, 666); + subon = evil; + ifhit = subon; + } else if (line.indexOf(';; ELIF ') === 0) { + line = line.substring(6).trim(); + let evil = print.constReplace(line, subst, 0, 666); + subon = !subon && evil; + ifhit = ifhit || subon; + } else if (line.indexOf(';; ELSE') === 0) { + subon = !ifhit && !subon; + } else if (line.indexOf(';; END') === 0) { + subon = true; + ifhit = false; + } else if (subon) { + append(print.constReplace(line, subst, 0, pad)); + } + } + + function appendAll(arr) { + if (!arr) return; + if (!Array.isArray(arr)) arr = [ arr ]; + arr.forEach(function(line) { append(line) }); + } + + function appendAllSub(arr, pad) { + if (!arr || arr.length === 0) return; + if (!Array.isArray(arr)) arr = [ arr ]; + arr.forEach(function(line) { appendSub(line, pad) }); + } + + function appendTok() { + let arg = [...arguments].map(v => { + return typeof(v) === 'object' ? JSON.stringify(v) : v; + }); + append(arg.join('')); + } + + function preamble() { + append(`; Generated by Kiri:Moto ${kiri.version}`); + append(`; ${new Date().toString()}`); + appendSub("; Bed left:{left} right:{right} top:{top} bottom:{bottom}"); + append(`; Bed type: ${bedType}`); + append(`; Target: ${filter[mode]}`); + // inject thumbnail preview + if (exportThumb && worker.snap) const { let { width, height, url } = worker.snap;;let data = url.substring(url.indexOf(',') + 1); + append(`; thumbnail begin ${width} ${height} ${data.length}`); + for (let i=0; i" comment + let pre = 0; + let gcpre = []; + for (let line of device.gcodePre) { + line = line.trim(); + if (line.indexOf(";; PREAMBLE") === 0) { + if (line === ';; PREAMBLE OFF') pre = 1; + else if (line === ';; PREAMBLE END') pre = 2; + else gcpre.push(pre = 123); + } else if (line.indexOf(";; AXISMAP ") === 0) { + let axmap = JSON.parse(line.substring(11).trim()); + for (let key in axmap) { + axis[key] = ` ${axmap[key]}`; + } + } else { + gcpre.push(line); + } + } + + if (pre === 0) preamble(); + + let t0 = false; + let t1 = false; + for (let line of gcpre) { + if (line === pre) { + preamble(); + continue; + } + if (line.indexOf('T0') === 0) t0 = true; else + if (line.indexOf('T1') === 0) t1 = true; else + if (line.indexOf('M82') === 0) { + extrudeAbs = true; + extrudeSet = true; + } else + if (line.indexOf('M83') === 0) { + extrudeAbs = false; + extrudeSet = true; + } else + if (line.indexOf('G90') === 0 && !extrudeSet) extrudeAbs = true; else + if (line.indexOf('G91') === 0 && !extrudeSet) extrudeAbs = false; else + if (line.indexOf('G92') === 0) { + line.split(";")[0].split(' ').forEach(function (tok) { + let val = parseFloat(tok.substring(1) || 0) || 0; + switch (tok[0]) { + case 'X': pos.x = val; break; + case 'Y': pos.y = val; break; + case 'Z': pos.z = val; break; + case 'E': outputLength = val; break; + } + }); + } + if (line.indexOf('E') > 0) { + line.split(";")[0].split(' ').forEach(function (tok) { + // use max E position from gcode-preamble + if (tok[0] == 'E') { + if (extrudeAbs) { + outputLength = Math.max(outputLength, parseFloat(tok.substring(1)) || 0); + emitted = outputLength; + } else { + emitted += parseFloat(tok.substring(1) || 0); + } + } + }); + } + appendSubPad(line); + } + + if (pre === 2) preamble(); + + function dwell(ms) { + time += ms/1000; + // break up dwell times over 4s because of + // limitations in some firmwares + while (ms > 0) { + let wait = Math.min(4000, ms); + append(`G4 P${wait}`); + ms -= wait; + if (ms) { + append('G1'); + } + } + } + + function retract(zhop) { + if (retracted) { + // console.log({double_retract: zhop}); + return; + } + retracted = retDist; + if (fwRetract) { + append('G10'); + } else { + moveTo({e:-retracted}, retSpeed, `e-retract ${retDist}`); + } + if (zhop) moveTo({z:zpos + zhop}, seekMMM, "z-hop start"); + time += (retDist / retSpeed) * 60 * 2; // retraction time + } + + function unretract() { + if (!retracted) { + return; + } + drainQ(); + // console.log({engage:zhop}); + // when enabled, resume previous Z + if (zhop && pos.z != zpos) moveTo({z:zpos}, seekMMM, "z-hop end"); + // re-engage retracted filament + if (fwRetract) { + append('G11'); + } else { + moveTo({e:retracted}, retSpeed, `e-engage ${retracted}`); + } + retracted = 0; + // optional dwell after re-engaging filament to allow pressure to build + if (retDwell) dwell(retDwell); + time += (retDist / retSpeed) * 60 * 2; // retraction time + } + + let taxis = new THREE.Vector3( 1, 0, 0 ); + let tcent = new THREE.Vector2( 0, 0 ); + let angle = -Math.PI / 4; + let savePos = pos; + + function pushPos(newpos, rate, comment) { + savePos = Object.clone(pos); + moveTo({ + x: newpos.x + offset.x, + y: newpos.y + offset.y, + z: newpos.z + offset.z + }, rate, comment); + } + + function popPos(rate, comment) { + moveTo(savePos, rate, comment); + } + + function moveTo(newpos, rate, comment) { + let o = [!rate && !newpos.e ? 'G0' : 'G1']; + let emit = { x: false, y: false, z: false }; + if (typeof newpos.x === 'number' && newpos.x !== pos.x) { + pos.x = newpos.x; + emit.x = true; + } + if (typeof newpos.y === 'number' && newpos.y !== pos.y) { + pos.y = newpos.y; + emit.y = true; + if (isBelt) emit.z = true; + } + if (typeof newpos.z === 'number' && newpos.z !== pos.z) { + pos.z = newpos.z; + emit.z = true; + if (isBelt) emit.y = true; + } + let epos = isBelt ? { x: pos.x, y: pos.y, z: pos.z } : pos; + if (isBelt) { + let zheight = path ? path.height || 0 : 0; + epos.x = originCenter ? -pos.x : device.bedWidth - pos.x; + epos.z = blastz = pos.z * icos; + epos.y = -pos.y + epos.z * bcos + belt_add_y; + lout = epos; + } + if (emit.x) o.append(axis.X).append(epos.x.toFixed(decimals)); + if (emit.y) o.append(axis.Y).append(epos.y.toFixed(decimals)); + if (emit.z) o.append(axis.Z).append(epos.z.toFixed(decimals)); + if (debug) { + if (emit.x) minz.x = Math.min(minz.x, epos.x); + if (emit.y) minz.y = Math.min(minz.y, epos.y); + if (emit.z) minz.z = Math.min(minz.z, epos.z); + } + if (typeof newpos.e === 'number') { + outputLength += newpos.e; + if (extrudeAbs) { + // for cumulative (absolute) extruder positions + o.append(axis.E).append(outputLength.toFixed(decimals)); + } else { + o.append(axis.E).append(newpos.e.toFixed(decimals)); + } + } + if (zMoveMax && emit.z) { + rate = Math.min(zMoveMax, rate) * 60; + } + if (rate && rate != pos.f) { + o.append(" F").append(Math.round(rate)); + pos.f = rate + } + if (comment) { + o.append(` ; ${comment}`); + } + if (o.length === 1) { + // console.trace({no_move: o, out, newpos, pos, lastp, emit}); + return; + } + let line = o.join(''); + if (last == line) { + // console.log({dup:line}); + return; + } + last = line; + subst.pos_x = epos.x.round(decimals); + subst.pos_y = epos.y.round(decimals); + subst.pos_z = epos.z.round(decimals); + append(line); + } + + // calc total distance traveled by head as proxy for progress + let allout = []; + let totaldistance = 0; + layers.forEach(function(outs) { + allout.appendAll(outs); + }); + allout.forEachPair(function (o1, o2) { + totaldistance += o1.point.distTo2D(o2.point); + }, 1); + + // retract before first move + retract(); + + while (layer < layers.length) { + path = layers[layer]; + layerno = path.slice.index; + // range overrides + if (path.layer >= 0) { + updateParams(path.layer, path.params); + } + + emitPerMM = extrudePerMM( + lineWidth || extruder.extNozzle, + extruder.extFilament, + path.layer === 0 ? + (process.firstSliceHeight || process.sliceHeight) : path.height); + + zpos = path.z || zpos; + bmax = Math.max(bmax, pos.z * icos); + subst.z = subst.Z = zpos.round(3); + subst.e = subst.E = outputLength; + subst.layer = layer; + subst.height = path.height; + + if (isBelt) { + pos.z = zpos; + } + + if (gcodePause && pause.indexOf(layer) >= 0) { + appendAllSub(gcodePause) + } + + let endloop = 0; + if (loops) { + for (let loop of loops) { + if (layerno === loop.start && !loop.started) { + loop.started = true; + append(`M808 L${loop.iter}`); + if (extrudeAbs) { + append(`G92 Z${lout.z.round(decimals)} E${outputLength.round(decimals)}`); + } else { + append(`G92 Z${lout.z.round(decimals)}`); + } + inloops++; + } + if (layerno === loop.end && !loop.ended) { + loop.ended = true; + endloop++; + inloops--; + } + } + } + + if (gcodeLayer && gcodeLayer.length) { + appendAllSub(gcodeLayer); + } else { + append(`;; --- layer ${layer} (${subst.height.toFixed(3)} @ ${subst.z.round(3)}) ---`); + } + + // layer temp and fan overrides at layer changes + if (fanSpeed !== lastFanSpeed) { + appendAllSub(gcodeFan); + lastFanSpeed = fanSpeed; + } + if (bedTemp !== lastBedTemp) { + append(`M140 S${bedTemp} T0`); + lastBedTemp = bedTemp; + } + if (nozzleTemp !== lastNozzleTemp) { + if (t0) append(`M104 S${nozzleTemp} T0`); + if (t1) append(`M104 S${nozzleTemp} T1`); + if (!(t0 || t1)) append(`M104 S${nozzleTemp} T${tool}`); + lastNozzleTemp = nozzleTemp; + } + + // move Z to layer height + if (layer > 0 || !isBelt) { + moveTo({z:zpos}, seekMMM); + } + + let cwidget; + // iterate through layer outputs + for (pidx=0; pidx 1) { + let el = arcQ.length; + deem = arcQ[0].e !== rec.e; + depm = arcQ[0].emitPerMM !== rec.emitPerMM; + desp = arcQ[0].speedMMM !== rec.speedMMM; + } + // ondebug({arcQ}); + if (arcQ.length > 2) { + let el = arcQ.length; + let e1 = arcQ[0]; // first in arcQ + let e2 = arcQ[Math.floor(el/2)]; // mid in arcQ + let e3 = arcQ[el-1]; // last in arcQ + let e4 = arcQ[el-2]; // second last in arcQ + let e5 = arcQ[el-3]; // third last in arcQ + let cc = util.center2d(e1, e2, e3, 1); // find center + let lr = util.center2d(e3, e4, e5, 1); // find local radius + let dc = 0; + + let radFault = false; + if (lr) { + let angle = 2 * Math.asin(dist/(2*lr.r)); + radFault = Math.abs(angle) > Math.PI * 2 / arcRes; // enforce arcRes(olution) + // if (arcQ.center) { + // arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); + // let avg = arcQ.rSum / arcQ.center.length; + // radFault = radFault || Math.abs(avg - lr.r) / avg > arcDev; // eliminate sharps and flats when local rad is out of arcDev(iation) + // } + } else { + radFault = true; + } + + if (cc) { + if ([cc.x,cc.y,cc.z,cc.r].hasNaN()) { + console.log({cc, e1, e2, e3}); + } + if (arcQ.length === 3) { + arcQ.center = [ cc ]; + arcQ.xSum = cc.x; + arcQ.ySum = cc.y; + arcQ.rSum = cc.r; + } else { + // check center point delta + arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); + arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); + arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); + let dx = cc.x - arcQ.xSum / arcQ.center.length; + let dy = cc.y - arcQ.ySum / arcQ.center.length; + dc = Math.sqrt(dx * dx + dy * dy); + } + + // if new point is off the arc + // if (deem || depm || desp || dc > arcDist || cc.r < arcMin || cc.r > arcMax || dist > cc.r) { + if (deem || depm || desp || dc * arcQ.center.length / arcQ.rSum > arcDist || dist > cc.r || cc.r > arcMax || radFault || !arcValid()) { + // let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcDist, dist > cc.r, cc.r > arcMax, radFault]; + if (arcQ.length === 4) { + // not enough points for an arc, drop first point and recalc center + emitQrec(arcQ.shift()); + let tc = util.center2d(arcQ[0], arcQ[1], arcQ[2], 1); + // the new center is invalid as well. drop the first point + if (!tc) { + emitQrec(arcQ.shift()); + } else { + arcQ.center = [ tc ]; + let angle = 2 * Math.asin(arcQ[1].dist/(2*tc.r)); + if (Math.abs(angle) > Math.PI * 2 / arcRes) { // enforce arcRes on initial angle + emitQrec(arcQ.shift()); + } + } + } else { + // enough to consider an arc, emit and start new arc + let defer = arcQ.pop(); + drainQ(); + // re-add point that was off the last arc + arcQ.push(defer); + } + } else { + // new point is on the arc + arcQ.center.push(cc); + } + } else { + // drainQ on invalid center + drainQ(); + } + } + } else { + // emitMM = emitPerMM * out.emit * dist; + emitMM = extrudeMM(dist, emitPerMM, out.emit); + moveTo({x:x, y:y, e:emitMM}, speedMMM); + emitted += emitMM; + } + } else { + drainQ(); + moveTo({x:x, y:y}, seekMMM); + // TODO disabling out of plane z moves until a better mechanism + // can be built that doesn't rely on computed zpos from layer heights... + // when making z moves (like polishing) allow slowdown vs fast seek + // let moveSpeed = (lastp && lastp.z !== z) ? speedMMM : seekMMM; + // moveTo({x:x, y:y, z:z}, moveSpeed); + } + + // retract filament if point retract flag set + if (out.retract) { + drainQ(); + retract(zhop); + } + + // update time and distance (should calc in moveTo() instead) + if (dist && speedMMM) { + time += (dist / speedMMM) * 60 * timeFactor; + } + // if (isNaN(time)) { + // console.log({ layer, path, dist, speedMMM, timeFactor }); + // throw "NaN"; + // } + distance += dist; + subst.progress = progress = Math.round((distance / totaldistance) * 100); + + // emit tracked progress + if (gcodeTrack && progress != lastProgress) { + appendAllSub(gcodeTrack); + lastProgress = progress; + } + + lastp = out.point; + laste = out.emit; + } + layer++; + if (cwidget) { + append(`; end object id: ${cwidget.track.grid_id}`); + isBambu && append('M625'); + cwidget = undefined; + } + + // end open loop when detected + while (endloop-- > 0) { + append(`M808`); + } + drainQ(); + } + + function emitQrec(rec) const { let {e, x, y, dist, emitPerMM, speedMMM } = rec;;// emitMM = emitPerMM * e * dist; + emitMM = extrudeMM(dist, emitPerMM, e); + moveTo({x:x, y:y, e:emitMM}, speedMMM); + emitted += emitMM; + } + + function drainQ() { + if (!arcDist) { + return; + } + if (arcQ.length > 4) { + // ondebug({arcQ}); + let vec1 = new THREE.Vector2(arcQ[1].x - arcQ[0].x, arcQ[1].y - arcQ[0].y); + let vec2 = new THREE.Vector2(arcQ.center[0].x - arcQ[0].x, arcQ.center[0].y - arcQ[0].y); + let gc = vec1.cross(vec2) < 0 ? 'G2' : 'G3'; + let from = arcQ[0]; + let to = arcQ.peek(); + arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); + arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); + arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); + let cl = arcQ.center.length; + let cc; + + let angle = util.thetaDiff( + Math.atan2((from.y - arcQ.ySum / cl), (from.x - arcQ.xSum / cl)), + Math.atan2((to.y - arcQ.ySum / cl), (to.x - arcQ.xSum / cl)), + gc === "G2" + ); + + if (Math.abs(angle) <= 3 * Math.PI / 4) { + cc = util.center2pr(from, to, arcQ.rSum / cl, gc === "G3"); + } + + if (!cc) { + cc = {x:arcQ.xSum/cl, y:arcQ.ySum/cl, z:arcQ[0].z, r:arcQ.rSum/cl}; + } + + // first arc point + emitQrec(from); + // console.log(arcQ.slice(), arcQ.center); + // console.log({first: from, last: arcQ.peek(), center: cc}); + // rest of arc to final point + let dist = arcQ.slice(1).map(v => v.dist).reduce((a,v) => a+v); + let emit = from.e;//arcQ.slice(1).map(v => v.e).reduce((a,v) => a+v); + emit = (from.emitPerMM * emit * dist); + outputLength += emit; + emitted += emit; + if (extrudeAbs) { + emit = outputLength; + } + // XYR form + // let pre = `${gc} X${to.x.toFixed(decimals)} Y${to.y.toFixed(decimals)} R${cc.r.toFixed(decimals)} E${emit.toFixed(decimals)}`; + // XYIJ form + let pre = `${gc} X${to.x.toFixed(decimals)} Y${to.y.toFixed(decimals)} I${(cc.x - pos.x).toFixed(decimals)} J${(cc.y - pos.y).toFixed(decimals)} E${emit.toFixed(decimals)}`; + let add = pos.f !== from.speedMMM ? ` E${from.speedMMM}` : ''; + append(`${pre}${add} ; merged=${cl-1} len=${dist.toFixed(decimals)} cp=${cc.x.round(2)},${cc.y.round(2)}`); + pos.x = to.x; + pos.y = to.y; + pos.z = to.z; + } else { + for (let rec of arcQ) { + emitQrec(rec); + } + } + arcQ.length = 0; + arcQ.center = undefined; + } + + // comprehensive arc validator + function arcValid() { + if (arcQ.length < 3) { + return false; + } + + let globalCenters = []; // see how a point first the curve within the context of the arc's end points. + + for (let i = 0; i < arcQ.length - 2; i++) { + let cc = util.center2d(arcQ[0], arcQ[i+1], arcQ[arcQ.length - 1], 1); + if (!cc) { + return false; + } + globalCenters.push(cc); + } + + let ac = { // average center + x:globalCenters.reduce( (t,v) => t + v.x , 0) / (globalCenters.length), + y:globalCenters.reduce( (t,v) => t + v.y , 0) / (globalCenters.length), + z:globalCenters.reduce( (t,v) => t + v.z , 0) / (globalCenters.length), + r:globalCenters.reduce( (t,v) => t + v.r , 0) / (globalCenters.length) + }; + + // make sure centers are within specified tolerance + for (let cc of globalCenters) { + let dc = Math.sqrt(Math.pow(cc.x - ac.x, 2) + Math.pow(cc.y - ac.y, 2)); + if (dc / ac.r > arcDist) { + return false; + } + } + + // make sure radii are within specified tolerance + for (let point of arcQ) { + let rad = Math.sqrt(Math.pow(point.x - ac.x, 2) + Math.pow(point.y - ac.y, 2)); + if (Math.abs(rad - ac.r) > ac.r * arcDist) { + return false; + } + } + + // enforce arcRes(olution) + for (let i = 1; i < arcQ.length; i++) { + let angle = 2 * Math.asin(arcQ[i].dist/(2*ac.r)); + if (Math.abs(angle) > Math.PI * 2 / arcRes) { + return false; + } + } + + // check points in the context of neighbors + for (let i = 0; i < arcQ.length - 2; i++) { + let cc = util.center2d(arcQ[i], arcQ[i+1], arcQ[i+2], 1); + if (!cc || Math.abs((cc.r - ac.r) / cc.r) > arcDev) { + return false; + } + } + + return true; + + } + + if (inloops) { + append(`M808`); + } + + segments.push({emitted, tool}); + subst.material = util.round(emitted,2); + subst.time = util.round(time,2); + subst['print_time'] = subst.time; + + append("; --- shutdown ---"); + appendAllSub(device.gcodePost); + append(`; --- filament used: ${subst.material} mm ---`); + append(`; --- print time: ${time.toFixed(0)}s ---`); + + // force emit of buffer + append(); + // console.log({segments, emitted, outputLength}); + print.distance = emitted; + print.lines = lines; + print.bytes = bytes + lines - 1; + print.time = time; + print.labels = model_labels; + + if (debug) { + console.log('segments', segments); + console.log('minz', minz); + } +}; + + +export { encodeBitOffset, setTempFanSpeed, updateParams, append, appendSubPad, appendSub, appendAll, appendAllSub, appendTok, preamble, dwell, retract, unretract, pushPos, popPos, moveTo, emitQrec, drainQ, arcValid, debug, model_labels, layers, tools_used, bytes, byteIndex, bitPosition, rloops, loops, subon, ifhit, evil, arg, data, i, pre, gcpre, axmap, t0, t1, wait, taxis, tcent, angle, savePos, o, emit, epos, zheight, line, allout, totaldistance, endloop, off, b64, x, rec, deem, depm, desp, el, e1, e2, e3, e4, e5, cc, lr, dc, radFault, avg, dx, dy, tc, defer, moveSpeed, vec1, vec2, gc, from, to, cl, dist, add, globalCenters, ac, rad }; \ No newline at end of file diff --git a/src2/kiri-mode/fdm/fill.js b/src2/kiri-mode/fdm/fill.js new file mode 100644 index 00000000..69155b11 --- /dev/null +++ b/src2/kiri-mode/fdm/fill.js @@ -0,0 +1,325 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { base } from '../geo/base.js'; +import { gyroid } from '../geo/gyroid.js'; +import { driver } from '../kiri-mode/fdm/driver.js'; + + +const { base, kiri } = root; +const { util } = base; + +const DEG2RAD = Math.PI / 180, + FILL = kiri.fill = { + hex: fillHexFull, + grid: fillGrid, + gyroid: fillGyroid, + triangle: fillTriangle, + linear: fillLinear, + cubic: fillCubic + }, + CACHE = kiri.fill_fixed = { + hex: fillHexFull, + grid: fillGrid, + triangle: fillTriangle + }; + +function fillHexFull(target) { + fillHex(target, true); +} + +/** + * emitter creates a hex infill pattern and sends to target + * + * @param {Object} target + * @param {boolean} full continuous walls + */ +function fillHex(target, full) { + // compute segment lengths (vert/horiz and 45) + let spacing = target.offset(); + // let vhlen = (1 / target.density()) * (target.lineWidth() + spacing); + let vhlen = (1 / target.density()) * target.lineWidth() * 0.5; + let anxlen = (Math.cos(30 * DEG2RAD) * vhlen).round(7); + let anylen = (Math.sin(30 * DEG2RAD) * vhlen).round(7); + let bounds = target.bounds(); + let even = true; + let evenZ = target.zIndex() % 2 === 0; + let maxy = bounds.max.y + (vhlen + anylen * 2); + let x, y; + + if (full || evenZ) { + x = bounds.min.x; + for (;;) { + if (even && x > bounds.max.x) break; + if (!even && x > bounds.max.x + anxlen + spacing) break; + y = bounds.min.y; + target.newline(); + while (y <= maxy) { + target.emit(x,y); + y += vhlen; + target.emit(x,y); + if (even) x += anxlen; else x -= anxlen; + y += anylen; + target.emit(x,y); + y += vhlen; + target.emit(x,y); + if (even) x -= anxlen; else x += anxlen; + y += anylen; + } + x += spacing; + if (even) x += (anxlen * 2); + even = !even; + target.newline(); + } + } else { + y = bounds.min.y + vhlen; + for (;;) { + if (even && y > bounds.max.y) break; + if (!even && y > bounds.max.y + anylen) break; + x = bounds.min.x; + target.newline(); + while (x < bounds.max.x) { + target.emit(x,y); + if (even) y += anylen; else y -= anylen; + x += anxlen; + target.emit(x,y); + x += spacing; + target.emit(x,y); + if (even) y -= anylen; else y += anylen; + x += anxlen; + target.emit(x,y); + x += spacing; + } + y += vhlen; + if (even) y += (anylen * 2); + even = !even; + target.newline(); + } + } +} + +function fillGyroid(target) { + let bounds = target.bounds(); + let height = target.zHeight(); + let span_x = bounds.max.x - bounds.min.x; + let span_y = bounds.max.y - bounds.min.y; + let density = target.density(); + let tile = 1 + (1 - density) * 15; + let tile_x = span_x / tile; + let tile_y = span_y / tile; + let tile_z = 1 / tile; + let gyroid = base.gyroid.slice(target.zValue() * tile_z, (1 - density) * 500); + + let polys = []; + if (gyroid.dir == 'lr') { + for (let ty=0; ty<=tile_y; ty++) { + for (let tx=0; tx<=tile_x; tx++) { + for (let poly of gyroid.polys) { + target.newline(); + let points = poly.map(el => { + return { + x: el.x * tile + tx * tile + bounds.min.x, + y: el.y * tile + ty * tile + bounds.min.y, + z: 0 + } + }); + polys.push(base.newPolygon().setOpen(true).addObj(points)); + } + } + polys = connectOpenPolys(polys); + } + } else { + for (let tx=0; tx<=tile_x; tx++) { + for (let ty=0; ty<=tile_y; ty++) { + for (let poly of gyroid.polys) { + target.newline(); + let points = poly.map(el => { + return { + x: el.x * tile + tx * tile + bounds.min.x, + y: el.y * tile + ty * tile + bounds.min.y, + z: 0 + } + }); + polys.push(base.newPolygon().setOpen(true).addObj(points)); + } + } + polys = connectOpenPolys(polys); + } + } + for (let poly of polys.filter(p => p.perimeter() > 2)) { + target.newline(); + for (let point of poly.points) { + target.emit(point.x, point.y); + } + } +} + +function connectOpenPolys(noff, dist = 0.1) { + if (noff.length <= 1) { + return noff; + } + let heal = 0; + // heal/rejoin open segments that have close endpoints + outer: for(;; heal++) { + let ntmp = noff, tlen = ntmp.length; + for (let i=0; i 0) { + // cull nulls + noff = noff.filter(o => o); + } + return noff; +} + +function fillGrid(target) { + let bounds = target.bounds(); + let height = target.zHeight(); + let span_x = bounds.max.x - bounds.min.x; + let span_y = bounds.max.y - bounds.min.y; + let offset = target.offset() / 2; + let tile = (1 / target.density()) * target.lineWidth(); + let tile_x = tile + offset; + let tile_xc = span_x / tile_x; + let tile_yc = span_y / tile; + + for (let tx=0; tx<=tile_xc; tx++) { + target.newline(); + for (let ty=0; ty<=tile_yc; ty++) { + let bx = tx * tile_x + bounds.min.x; + let by = ty * tile + bounds.min.y; + if ((tx + ty) % 2) { + target.emit(bx, by); + target.emit(bx + tile_x - offset, by + tile); + } else { + target.emit(bx + tile_x - offset, by); + target.emit(bx, by + tile); + } + } + } +} + +function fillCubic(target) { + let bounds = target.bounds(); + let span = Math.max( + bounds.max.x - bounds.min.x, + bounds.max.y - bounds.min.y + ); + let steps = Math.floor((span / target.lineWidth()) * target.density()); + let step = span / steps; + let ztype = Math.floor(target.zIndex() / target.repeat()) % 3; + if (ztype === 1) { + for (let tx=bounds.min.x; tx<=bounds.max.x; tx += step) { + target.newline(); + target.emit(tx, bounds.min.y); + target.emit(tx, bounds.max.y); + } + } else if (ztype === 0) { + for (let ty=bounds.min.y; ty<=bounds.max.y; ty += step) { + target.newline(); + target.emit(bounds.min.x, ty); + target.emit(bounds.max.x, ty); + } + } else { + step *- Math.sqrt(2); + for (let tx=bounds.min.x; tx<=bounds.max.x; tx += step) { + target.newline(); + target.emit(tx, bounds.min.y); + target.emit(tx + 1000, bounds.max.y + 1000); + } + for (let ty=bounds.min.y; ty<=bounds.max.y; ty += step) { + target.newline(); + target.emit(bounds.min.x, ty); + target.emit(bounds.min.x + 1000, ty + 1000); + } + } +} + +function fillLinear(target) { + let bounds = target.bounds(); + let span = Math.max( + bounds.max.x - bounds.min.x, + bounds.max.y - bounds.min.y + ); + let steps = Math.floor((span / target.lineWidth()) * target.density()); + let step = span / steps; + let ztype = Math.floor(target.zIndex() / target.repeat()) % 2; + if (ztype === 1) { + for (let tx=bounds.min.x; tx<=bounds.max.x; tx += step) { + target.newline(); + target.emit(tx, bounds.min.y); + target.emit(tx, bounds.max.y); + } + } else if (ztype === 0) { + for (let ty=bounds.min.y; ty<=bounds.max.y; ty += step) { + target.newline(); + target.emit(bounds.min.x, ty); + target.emit(bounds.max.x, ty); + } + } +} + +function fillTriangle(target) { + let bounds = target.bounds(); + let span_x = bounds.max.x - bounds.min.x; + let span_y = bounds.max.y - bounds.min.y; + let offset = target.offset(); + let line_w = target.lineWidth() / 2; + let tile = (1 / target.density()) * (target.lineWidth() * 1.25); + let tile_x = tile + offset*2 + line_w; + let tile_xc = span_x / tile_x; + let tile_yc = span_y / tile; + + for (let tx=0; tx<=tile_xc; tx++) { + target.newline(); + for (let ty=0; ty<=tile_yc; ty++) { + let bx = tx * tile_x + bounds.min.x; + let by = ty * tile + bounds.min.y; + if ((tx + ty) % 2) { + target.emit(bx, by); + target.emit(bx + tile_x - offset - line_w, by + tile); + } else { + target.emit(bx + tile_x - offset - line_w, by); + target.emit(bx, by + tile); + } + } + } + + for (let tx=0; tx<=tile_xc; tx++) { + let bx = tx * tile_x + bounds.min.x; + let xp = bx + tile_x - line_w/2 - offset/2; + target.newline(); + target.emit(xp, bounds.min.y); + target.emit(xp, bounds.max.y); + } +} + + +export { fillHexFull, fillHex, fillGyroid, connectOpenPolys, fillGrid, fillCubic, fillLinear, fillTriangle, DEG2RAD, spacing, vhlen, anxlen, anylen, bounds, even, evenZ, maxy, height, span_x, span_y, density, tile, tile_x, tile_y, tile_z, gyroid, polys, ty, tx, points, heal, ntmp, i, s1, j, s2, offset, tile_xc, tile_yc, bx, by, span, steps, step, ztype, line_w, xp }; diff --git a/src2/kiri-mode/fdm/post.js b/src2/kiri-mode/fdm/post.js new file mode 100644 index 00000000..ff3c902f --- /dev/null +++ b/src2/kiri-mode/fdm/post.js @@ -0,0 +1,321 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { base } from '../geo/base.js'; +import { polygons } from '../geo/polygons.js'; +import { driver } from '../kiri-mode/fdm/driver.js'; + + +const { base, kiri } = root; +const { driver } = kiri; +const { polygons } = base; +const { fillArea } = polygons; +const { FDM } = driver; + +const POLY = polygons; + +/** + * may run in minion or worker context. do not create objects + * that will not quickly encode in threaded mode. add to existing + * data object. return is ignored. + */ +base.slicePost.FDM = function(data, options) { + const { z, lines, groups } = data; + const { useAssembly, post_args, zIndexes } = options; + const { process, isSynth, vaseMode } = post_args; + const { shellOffset, fillOffset, clipOffset } = post_args; + data.tops = POLY.nest(groups); + if (isSynth) { + const process = post_args.process; + if (process.sliceSupportGrow > 0) { + // expand synth supports here so they can be clipped later + data.tops = POLY.expand(data.tops, process.sliceSupportGrow, data.z, []); + } + // do not shell synth widgets because + // they will be clipped against peers later + // which requires shelling post-clip + // but we still need to generate tops + delete data.groups; + return; + } + const index = zIndexes.indexOf(z); + const range = FDM.getRangeParameters(process, index); + // calculate fractional shells + let shellFrac = (range.sliceShells - (range.sliceShells | 0)); + let sliceShells = range.sliceShells | 0; + if (shellFrac) { + let v1 = shellFrac > 0.5 ? 1 - shellFrac : shellFrac; + let v2 = 1 - v1; + let parts = Math.round(v2/v1) + 1; + let rem = index % parts; + let trg = shellFrac > 0.5 ? 1 : parts - 1; + sliceShells += rem >= trg ? 1 : 0; + } + const isFirst = index === 0; + const height = process.sliceHeight; + const spaceMult = isFirst ? process.firstLayerLineMult || 1 : 1; + const count = isSynth ? 1 : sliceShells; + const offset = shellOffset * spaceMult; + const fillOff = fillOffset * spaceMult; + const thinType = isSynth ? undefined : process.sliceDetectThin; + const nutops = []; + // co-locate shell processing with top generation in slicer + for (let top of data.tops) { + nutops.push(FDM.doTopShells(z, top, count, offset/2, offset, fillOff, { + thinType, + vaseMode, + useAssembly + })); + } + data.clip = clipOffset ? POLY.offset(nutops.map(t => t.simple), clipOffset) : undefined; + data.tops = nutops; + delete data.groups; +}; + +function offset_default(params) { + let { z, top, count, top_poly, offset1, offsetN, wasm, last, gaps } = params; + // standard wall offsetting strategy + POLY.offset( + top_poly, + [-offset1, -offsetN], + { + z, + wasm, + count, + outs: top.shells, + flat: true, + call: (polys, onCount) => { + last = polys; + // mark each poly with depth (offset #) starting at 0 + for (let p of polys) { + p.depth = count - onCount; + if (p.fill_off) p.fill_off.forEach(function(pi) { + // use negative offset for inners + pi.depth = -(count - onCount); + }); + // mark inner depth to match parent + if (p.inner) { + for (let pi of p.inner) { + pi.depth = p.depth; + } + } + } + } + } + ); + return { last, gaps }; +} + +function thin_type_1(params) { + let { z, top, count, top_poly, offsetN, last, gaps } = params; + top.thin_fill = []; + top.fill_sparse = []; + + let layers = POLY.inset(top_poly, offsetN, count, z, true); + last = layers.last().mid; + top.shells = layers.map(r => r.mid).flat(); + top.gaps = layers.map(r => r.gap).flat(); + + let off = offsetN; + let min = off * 0.75; + let max = off * 4; + + for (let poly of layers.map(r => r.gap).flat()) { + let centers = poly.centers(off/2, z, min, max, {lines:false}); + top.fill_sparse.appendAll(centers); + } + + return { last, gaps }; +} + +function thin_type_2(params) { + let { z, top, count, top_poly, offset1, offsetN, last, gaps, wasm } = params; + + top.gaps = gaps; + top.thin_fill = []; + let oso = {z, count, gaps: [], outs: [], minArea: 0.05, wasm}; + POLY.offset(top_poly, [-offset1, -offsetN], oso); + + oso.outs.forEach((polys, i) => { + polys.forEach(p => { + p.depth = i; + if (p.fill_off) { + p.fill_off.forEach(pi => pi.depth = i); + } + if (p.inner) { + for (let pi of p.inner) { + pi.depth = p.depth; + } + } + top.shells.push(p); + }); + last = polys; + }); + + // slice.solids.trimmed = slice.solids.trimmed || []; + oso.gaps.forEach((polys, i) => { + let off = (i == 0 ? offset1 : offsetN); + polys = POLY.offset(polys, -off * 0.8, {z, minArea: 0, wasm}); + top.thin_fill.appendAll(cullIntersections( + fillArea(polys, 45, off/2, [], 0.01, off*2), + fillArea(polys, 135, off/2, [], 0.01, off*2), + )); + gaps.push(...polys); + }); + + return { last, gaps }; +} + +function thin_type_3(params) { + let { z, top, count, offsetN, last, gaps } = params; + top.thin_fill = []; + top.fill_sparse = []; + + let { noodle, remain } = top.poly.noodle(offsetN); + top.shells = noodle; + top.gaps = remain; + // top.fill_off = noodle; + + // if (remain.length) { + // top.gaps = []; + // for (let poly of remain) { + // let { noodle } = poly.noodle(offsetN); + // console.log({ next_noodle: noodle }); + // top.gaps.push(...noodle); + // } + // } + + return { last, gaps }; +} + +/** + * may run in minion or worker context. performs shell offsetting + * including thin wall detection when enabled + */ +FDM.doTopShells = function(z, top, count, offset1, offsetN, fillOffset, opt = {}) { + // pretend we're a top object in minions + if (!top.poly) { + top = { poly: top }; + } + + // add simple (low rez poly) where less accuracy is OK + top.simple = top.poly.simple(); + + let wasm = opt.useAssembly; + let top_poly = [ top.poly ]; + + if (opt.vaseMode) { + // remove top poly inners in vase mode + top.poly = top.poly.clone(false); + } + + top.shells = []; // strategies output shells here + top.fill_off = []; // offset from innermost shell for infill + top.fill_lines = []; // fill lines inside fill_off + + let last = [], + gaps = []; + + if (count) { + // permit offset of 0 for laser and drag knife + if (offset1 === 0 && count === 1) { + last = top_poly.clone(true); + top.shells = last; + } else { + // heal top open polygons if the ends are close (benchy tilt test) + top_poly.forEach(p => { if (p.open) { + let dist = p.first().distTo2D(p.last()); + if (dist < 1) p.open = false; + } }); + let ret = { last, gaps }; + switch (opt.thinType) { + case "type 1": + ret = thin_type_1({ z, top, count, top_poly, offsetN, fillOffset }); + break; + case "type 2": + ret = thin_type_2({ z, top, count, top_poly, offset1, offsetN, fillOffset, gaps, wasm }); + break; + case "type 3": + ret = thin_type_3({ z, top, count, top_poly, offsetN }); + fillOffset = 0; + break; + default: + ret = offset_default({ z, top, count, top_poly, offset1, offsetN, wasm }); + break; + } + last = ret.last || last; + gaps = ret.gaps || gaps; + } + } else { + // no shells, just infill, is permitted + last = [top.poly]; + } + + // generate fill offset poly set from last offset to top.fill_off + if (fillOffset && last.length > 0) { + // if gaps present, remove that area from fill inset + if (gaps.length) { + let nulast = []; + POLY.subtract(last, gaps, nulast, null, z); + last = nulast; + } + last.forEach(function(inner) { + POLY.offset([inner], -fillOffset, {outs: top.fill_off, flat: true, z}); + }); + } + + // for diffing + top.last = last; + // top.last_simple = last.map(p => p.clean(true, undefined, config.clipper / 10)); + + return top; +} + +/** + * given an array of arrays of points (lines), eliminate intersections + * between groups, then return a unified array of shortest non-intersects. + */ +function cullIntersections() { + function toLines(pts) { + let lns = []; + for (let i=0, il=pts.length; i t); + if (aOa.length < 1) return; + let aa = toLines(aOa.shift()); + while (aOa.length) { + let bb = toLines(aOa.shift()); + loop: for (let i=0, il=aa.length; i !l.del).concat(bb.filter(l => !l.del)); + } + let good = []; + for (let i=0, il=aa.length; i 2 ? good : []; +} + + +export { offset_default, thin_type_1, thin_type_2, thin_type_3, cullIntersections, toLines, POLY, process, index, range, shellFrac, sliceShells, v1, v2, parts, rem, trg, isFirst, height, spaceMult, count, offset, fillOff, thinType, nutops, layers, off, min, max, centers, oso, wasm, top_poly, last, dist, ret, nulast, lns, i, aOa, aa, bb, al, j, bl, good }; diff --git a/src2/kiri-mode/fdm/prepare.js b/src2/kiri-mode/fdm/prepare.js new file mode 100644 index 00000000..dc456fc0 --- /dev/null +++ b/src2/kiri-mode/fdm/prepare.js @@ -0,0 +1,1514 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { base } from '../geo/base.js'; +import { paths } from '../geo/paths.js'; +import { point } from '../geo/point.js'; +import { polygons } from '../geo/polygons.js'; +import { utils } from '../kiri/utils.js'; +import { slice } from '../kiri/slice.js'; +import { consts } from '../kiri/consts.js'; +import { render } from '../kiri/render.js'; +import { driver } from '../kiri-mode/fdm/driver.js'; + + +const { base, kiri } = root; +const { consts, driver, newSlice, render } = kiri; +const { polygons, paths, util, newPoint, newPolygon, Polygon } = base; +const { poly2polyEmit, tip2tipEmit } = paths; +const { numOrDefault } = util; +const { fillArea } = polygons; +const { beltfact } = consts; +const { FDM } = driver; +const { getRangeParameters } = FDM; +const POLY = polygons; + +/** + * DRIVER PRINT CONTRACT + * + * @param {Function} update progress callback + * @returns {Object[]} returns array of render objects + */ +FDM.prepare = async function(widgets, settings, update) { + // filter ignored widgets + widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); + + let { device, process, controller, bounds } = settings, + { sliceHeight, firstSliceHeight, firstLayerRate } = process, + { outputSeekrate, outputDraftShield, outputPurgeTower } = process, + { bedWidth, bedDepth } = device, + { lineType } = controller, + printPoint = newPoint(0,0,0), + print = self.worker.print = kiri.newPrint(settings, widgets), + nozzle = process.sliceLineWidth || device.extruders[0].extNozzle, + isBelt = device.bedBelt, + isThin = lineType === "line", + isFlat = lineType === "flat", + useRaft = process.outputRaft || false, + firstLayerHeight = isBelt ? sliceHeight : firstSliceHeight || sliceHeight, + layerno = 0, + zneg = 0, + zoff = 0, + zmin = 0, + shield, + output = [], + layerout = []; + + let lastLayerStart = null; + + // compute bounds if missing + if (!bounds) { + bounds = new THREE.Box3(); + for (let widget of widgets) { + let wp = widget.track.pos; + let wb = widget.bounds.clone(); + wb.min.x += wp.x; + wb.max.x += wp.x; + wb.min.y += wp.y; + wb.max.y += wp.y; + bounds.union(wb); + } + settings.bounds = bounds; + } + + // TODO pick a widget with a slice on the first layer and use that nozzle + // create brim, skirt, raft if specificed in FDM mode (code shared by laser) + if (!isBelt && (process.outputBrimCount || useRaft)) { + let brims = [], + offset = process.outputBrimOffset || (useRaft ? 4 : 0); + + // compute first brim + widgets.filter(w => w.slices.length).forEach(function(widget) { + let tops = []; + let slices = []; + if (outputDraftShield) { + slices.appendAll(widget.slices); + } else { + slices.push(widget.slices[0]); + } + for (let slice of slices) { + // collect top outer polygons + if (slice.tops) + slice.tops.forEach(function(top) { + tops.push(top.poly.clone()); + }); + // collect support polygons + if (slice.supports) + slice.supports.forEach(function(support) { + tops.push(support.clone()); + }); + } + if (outputDraftShield) { + tops = POLY.union(tops,1,true); + } + // nest and offset tops + POLY.nest(tops).forEach(function(poly) { + let off = poly.offset(-offset + nozzle / 2); + if (off) off.forEach(function(brim) { + brim.move(widget.track.pos); + brims.push(brim); + }); + }); + }); + + // merge brims + brims = POLY.union(brims, undefined, true); + + // if brim is offset, over-expand then shrink to induce brims to merge + if (brims.length > 1 && offset >= nozzle) { + let extra = process.sliceSupportExtra + 2; + let zheight = brims[0].getZ(); + brims = POLY.expand(brims, extra, zheight, null, 1); + brims = POLY.expand(brims, -extra, zheight, null, 1); + } + + // if raft is specified + if (useRaft) { + let offset = newPoint(0,0,0), + height = nozzle, + angle = process.sliceFillAngle, + rate1 = firstLayerRate / 3, + rate2 = process.outputFeedrate, + rafts = process.outputBrimCount ? + POLY.expand(brims, nozzle * 4, 0, null, 1) : brims; + + function raft(height, angle, spacing, speed, extrude, outline) { + let slice = newSlice(zoff + height / 2); + for (let brim of rafts) { + // use first point of first brim as start point + if (printPoint === null) printPoint = brim.first(); + let t = slice.addTop(brim); + if (outline) t.traces = [ brim ]; + t.inner = POLY.expand([ brim ], nozzle * -0.8, 0, null, 1); + t.fill_lines = fillArea(t.inner, angle, spacing, []); + t.isRaft = true; + } + offset.z = slice.z; + printPoint = slicePrintPath(print, slice, printPoint, offset, layerout, { + speed, + mult: extrude, + }); + layerout.z = zoff + height; + layerout.height = height; + output.append(layerout); + + layerout = []; + zoff += height; + zneg = height / 2; + }; + + // cause first point of raft to be used + printPoint = null; + + // emit raft layers + raft(nozzle * 1.5, angle + 0, nozzle * 6.00, rate1, 2.50, true); + raft(nozzle * 0.5, angle + 90, nozzle * 2.00, rate2, 1.50, true); + raft(nozzle * 0.5, angle + 0, nozzle * 1.00, rate2, 1.00); + raft(nozzle * 0.5, angle + 90, nozzle * 0.75, rate2, 0.75); + + // raise first layer off raft slightly to lessen adhesion + firstLayerHeight += process.outputRaftSpacing || 0; + zoff += process.outputRaftSpacing || 0; + + // retract after last raft layer + output.last().last().retract = true; + } + + // if using brim or skirt + if (process.outputBrimCount) { + let polys = [], + preout = []; + + // offset specified # of brims + POLY.offset(brims, nozzle, { + outs: polys, + flat: true, + count: process.outputBrimCount, + z: firstLayerHeight / 2 + zoff - zneg + }); + + print.setType('brim'); + shield = POLY.nest(polys.clone(), true).clone(); + + // output brim points + let brimStart = offset < nozzle * 2 ? newPoint(-bedWidth, -bedDepth, 0) : printPoint; + printPoint = poly2polyEmit(polys, brimStart, (poly, index, count, startPoint) => { + return print.polyPrintPath(poly, startPoint, preout, { + rate: firstLayerRate, + onfirst: function(point) { + if (preout.length && point.distTo2D(startPoint) > 2) { + // retract between brims + preout.last().retract = true; + } + } + }); + }); + + print.addPrintPoints(preout, layerout, null); + + if (preout.length) { + // retract between brims and print + preout.last().retract = true; + } + } + // recompute bounds for purge block offsets + let bbounds = base.newBounds(); + brims.forEach(brim => { + bbounds.merge(brim.bounds); + }); + bounds.min.x = Math.min(bounds.min.x, bbounds.minx); + bounds.min.y = Math.min(bounds.min.y, bbounds.miny); + bounds.max.x = Math.max(bounds.max.x, bbounds.maxx); + bounds.max.y = Math.max(bounds.max.y, bbounds.maxy); + } + + // synthesize support widgets when needed + // so that they can use a separate extruder + // compute zmin for belt purge towers + for (let widget of widgets.slice()) { + let sslices = []; + if (!widget.slices) { + console.log('invalid widget', widget); + continue; + } + for (let slice of widget.slices) { + zmin = Math.min(zmin, slice.z); + if (!slice.supports) { + continue; + } + let sslice = newSlice(slice.z); + sslice.extruder = process.sliceSupportNozzle; + sslice.supports = slice.supports.slice(); + sslice.height = slice.height; + sslices.push(sslice); + } + if (sslices.length) { + let swidget = kiri.newWidget(null,widget.group); + swidget.slices = sslices; + swidget.support = true; + swidget.belt = widget.belt; + swidget.track = Object.clone(widget.track); + swidget.mesh = { widget: swidget, position: swidget.track.pos }; + widget.anno.extruder = process.sliceSupportNozzle; + widgets.push(swidget); + } + } + + let lastPoly; + let lastLayer; + let lastOffset; + let extruders = print.extruders = []; + let extcount = 0; + + // find max layers (for updates) + // generate list of used extruders for purge blocks + for (let widget of widgets) { + let extruder = widget.anno.extruder ?? 0; + if (!extruders[extruder]) { + extruders[extruder] = {}; + extcount++; + } + } + + // determine size/location of purge blocks + let blokw = Math.sqrt(outputPurgeTower || 0); + let blokh = blokw; + let blokpos, walkpos, blok; + + function mkblok(w,h) { + let count = extcount - 1; + let gap = nozzle; + if (isBelt) { + let step = w + gap; + let mp = (bounds.max.y + bounds.min.y) / 2; // part y midpoint + let sp = (bounds.max.y - bounds.min.y); // part y span + let ts = (w * count) + (gap * (count - 1)); // tower y span + let ty = -ts + w/2 - gap/2; // tower start y + blokpos = { y:ty, x: bounds.max.x + 2 + h / 2}; // first block pos + walkpos = { y:step, x:0 }; + blok = { x:h, y:w }; + } else if (bounds.min.x < bounds.min.y) { + let step = h + gap; + let mp = (bounds.max.x + bounds.min.x) / 2; // part y midpoint + let sp = (bounds.max.x - bounds.min.x); // part y span + let ts = (h * count) + (gap * (count - 1)); // tower y span + let tx = mp - ts / 2 + step / 2; // tower start y + blokpos = { x:tx, y: bounds.max.y + 2 + w / 2}; // first block pos + walkpos = { x:step, y:0 }; + blok = { x:w, y:h }; + } else { + let step = w + gap; + let mp = (bounds.max.y + bounds.min.y) / 2; // part y midpoint + let sp = (bounds.max.y - bounds.min.y); // part y span + let ts = (w * count) + (gap * (count - 1)); // tower y span + let ty = mp - ts / 2 + step / 2; // tower start y + blokpos = { y:ty, x: bounds.max.x + 2 + h / 2}; // first block pos + walkpos = { y:step, x:0 }; + blok = { x:w, y:h }; + } + } + + function mkrec(i, angle = 45, thin = 6) { + let exi = device.extruders[i], + noz = exi.extNozzle, + pos = {x:blokpos.x + walkpos.x * i, y:blokpos.y + walkpos.y * i, z:0}, + rect = newPolygon().centerRectangle(pos, blok.x, blok.y), + full = linesToPoly(fillArea([ + newPolygon().centerRectangle(pos, blok.x - noz, blok.y - noz) + ], angle, noz)), + sparse = rect.area() > 10 ? linesToPoly(fillArea([ + newPolygon().centerRectangle(pos, blok.x - noz, blok.y - noz) + ], angle + 90, noz * thin)) : newPolygon(), + rec = { + extruder: i, + diameter: exi.extNozzle, + rect, + full, + sparse, + pause: newPoint(pos.x + walkpos.y, pos.y + walkpos.x, pos.z) + }; + return rec; + } + + mkblok(blokw, blokh); + + // allocate tower space for extruders-1 locations + let towers = extruders.slice(1).map((ext,i) => { + return ext ? mkrec(i) : ext; + }); + + function linesToPoly(points) { + let poly = newPolygon().setOpen(); + let ping = 0; + for (let i=0; i= 0 || lastPurgeTool === nozzle; + let tool = using >= 0 ? using : nozzle; + let first = isBelt ? !purgedFirst && layer.slice.index >= 0 : layer.slice.index === 0; + let rate = first ? process.firstLayerRate : process.outputFeedrate; + let wipe = true; + lastPurgeTool = tool; + if (rec) { + print.setType('purge tower'); + if (layer.last()) { + layer.last().retract = true; + } + purgedFirst = purgedFirst || first; + let purgeOn = first || !thin; + if (isBelt && z < 0) { + let scale = 1 - ((z / zmin)); + mkblok(blokw * scale, blokh); + rec = mkrec(rec.extruder, 0, 10); + rate = (process.outputFeedrate - process.firstLayerRate) * scale + process.firstLayerRate; + wipe = false; + } + let box = rec.rect.clone().setZ(z); + let pause = rec.pause.clone().setZ(z); + let fill = (z >= 0 && purgeOn ? rec.full : rec.sparse).clone().setZ(z); + if (isBelt) { + let bmove = {x:0, y:z, z:0}; + box.move(bmove); + pause.move(bmove); + if (fill) { + fill.move(bmove); + } + if (offset) { + let bo = { x:0, y:offset.y, z:offset.z }; + box.move(bo); + pause.move(bo); + if (fill) fill.move(bo); + } + } + start = print.polyPrintPath(box, start, layer, { + tool, + rate, + simple: true, + open: false, + onfirstout: (out => out.overate = (isBelt ? rate : 0)) + }); + if (fill && fill.length) { + // for pings, split path at 20mm + start = print.polyPrintPath(fill, start, layer, { + tool, + rate, + simple: true, + open: true, + onfirst: (point) => { point.purgeOn = purgeOn ? pause : undefined } + }); + } + layer.last().retract = true; + // experimental post-retract wipe + if (wipe) start = print.polyPrintPath(box, start, layer, { + tool, + rate, + simple: true, + open: false, + extrude: 0 + }); + layer.last().overate = 0; + return start; + } else { + console.log({no_purge_tower_for: nozzle, using, track, layer}); + return start; + } + } + + // establish offsets + for (let widget of widgets) { + let { belt } = widget; + let offset = Object.clone(widget.track.pos); + if (isBelt) { + offset = { + x: belt.xpos, + y: belt.ypos * belt.cosf, + z: belt.ypos * belt.sinf + }; + } else { + // when rafts used this is non-zero + offset.z = zoff; + } + widget.offset = offset; + } + + // create shuffled slice cake by z offset (slice.z + offset.z) + let cake = []; + let zrec = {}; + for (let widget of widgets) { + // skip synthesized support widget(s) + if (!widget.mesh) { + continue; + } + for (let slice of widget.slices) { + slice.widget = widget; + let z = (slice.z + widget.offset.z).round(2); + let rec = zrec[z] = zrec[z] || {z, slices:[]}; + if (rec.slices.length === 0) { + cake.push(rec); + } + rec.slices.push(slice); + } + } + cake.sort((a, b) => { + return a.z - b.z; + }); + + let firstTool; + let lastWidget; + let lastExt; + let lastOut; + + // walk cake layers bottom up + for (let layer of cake) { + // track purge blocks generated for each layer + let track = towers.slice(); + + // iterate over layer slices, find closest widget, print, eliminate + for (;;) { + let order = []; + // select slices of the same extruder type first then distance + for (let slice of layer.slices) { + if (slice.prep) { + continue; + } + let offset = lastOffset = slice.widget.offset; + let find = slice.findClosestPointTo(printPoint.sub(offset)); + if (find) { + let ext = slice.extruder; + let lex = lastExt; + let dst = Math.abs(find.distance); + // penalize extruder swaps + if (ext !== lex) { + dst *= 10000; + } + // for first object, penalize extruders other than first + if (lastExt === undefined && ext > 0) { + dst *= 10000; + } + order.push({dst, slice, offset, z: layer.z}); + } + } + if (order.length === 0) { + break; + } + order.sort((a,b) => { + return a.dst - b.dst; + }); + let { z, slice, offset } = order[0]; + if (firstTool === undefined) { + firstTool = slice.extruder; + } + + // when layers switch between widgets, force retraction + let forceRetract = lastOut && lastOut.widget !== slice.widget; + if (forceRetract && output.length) { + // selecet last output of last layer + output.last().last().retract = true; + } + + let params = getRangeParameters(process, slice.index); + slice.prep = true; + // retract between widgets or layers (when set) + if (layerout.length && slice.widget !== lastWidget) { + layerout.last().retract = true; + } + lastWidget = slice.widget; + layerout.z = z + slice.height / 2; + layerout.height = layerout.height || slice.height; + layerout.slice = slice; + layerout.params = params; + // mark layer as anchor if slice is belt and flag set + layerout.anchor = slice.belt && slice.belt.anchor; + // detect extruder change and print purge block + if (!lastOut || lastOut.extruder !== slice.extruder) { + if (slice.extruder >= 0) + printPoint = purge(slice.extruder, track, layerout, printPoint, slice.z, undefined, offset); + } + let wtb = slice.widget.track.box; + let beltStart = slice.belt && slice.belt.touch;// && (widgets.length === 1); + // output seek to start point between mesh slices if previous data + print.setType('layer'); + print.setWidget(lastWidget); + printPoint = slicePrintPath( + print, + slice, + beltStart ? newPoint(-5000, 5000, 0) : printPoint.sub(offset), + offset, + layerout, + { + routeAround: process.outputAvoidGaps, + seedPoint: printPoint.sub(offset), + params, // range parameters + first: slice.index === 0, + support: slice.widget.support, + onBelt: beltStart, + pretract: (wipeDist) => { + if (!(lastLayer && lastLayer.length)) { + return; + } + let lastOut = lastLayer.last().set_retract(); + if (wipeDist && lastPoly && lastOut.point) { + let center = lastPoly.center(true).add(offset); + let maxDist = lastOut.point.distTo2D(center); + let useDist = Math.min(wipeDist, maxDist); + let endpoint = lastOut.point.followTo(center, useDist); + if (endpoint.inPolygon(lastPoly)) { + print.addOutput(lastLayer, endpoint, null, null, lastOut.tool); + } + } + } + } + ); + print.setWidget(null); + + lastOut = slice; + lastExt = lastOut.extruder; + lastPoly = slice.lastPoly; + lastLayer = layerout; + + if (params.outputLayerRetract && layerout.length) { + layerout.last().retract = true; + } + } + + // clear slice.prep so it can be re-previewed in a different mode + for (let widget of widgets) { + // skip synthesized support widget(s) + if (!widget.mesh) { + continue; + } + for (let slice of widget.slices) { + slice.prep = false; + } + } + + // draft shield + if (layerno > 0 && shield && outputDraftShield) { + print.setType('shield'); + shield = POLY.setZ(shield.clone(), printPoint.z); + let preout = []; + printPoint = poly2polyEmit(shield, printPoint, (poly, index, count, startPoint) => { + return print.polyPrintPath(poly, startPoint, preout, { + onfirst: function(point) { + if (preout.length && point.distTo2D(startPoint) > 2) { + // retract between part and shield + preout.last().retract = true; + } + } + }); + }); + preout.last().retract = true; + layerout.appendAll(preout); + } + + // if a declared extruder isn't used in a layer, use selected + // extruder to fill the relevant purge blocks for later support + if (lastOut) track.slice().forEach(ext => { + printPoint = purge(ext.extruder, track, layerout, printPoint, lastOut.z, lastExt, lastOffset); + }); + + // if layer produced output, append to output array + if (layerout.length) { + output.append(layerout); + } + + // retract after last layer + if (layerno === cake.length - 1 && layerout.length) { + layerout.last().retract = true; + } + + // notify progress + layerout.layer = layerno++; + update((layerno / cake.length) * 0.5, "prepare"); + + layerout = []; + } + + print.output = output; + print.firstTool = firstTool; + + // post-process for base extrusions (touching the bed) + if (isBelt) { + // all widgets should have the same belt scaling constants + let belt = widgets[0].belt; + // tune base threshold + let thresh = Infinity; + for (let layer of output) { + for (let rec of layer) { + let point = rec.point; + thresh = Math.min(thresh, (belt.slope * point.z) - point.y); + } + } + // store this offset to be removed from Y values in export + print.belty = thresh; + thresh = Math.max(0, thresh) + firstLayerHeight * belt.slope; + // track last layer out b/c it might be connected to the current + // layer first point, in which case the injected brim will cause a long print line + let lastLayerOut; + // iterate over layers, find extrusion on belt and + // apply corrections and add brim when specified + for (let layer of output) { + let params = getRangeParameters(process, layer.layer || 0); + let brimHalf = params.firstLayerBrim < 0; + let firstLayerBrim = Math.abs(params.firstLayerBrim); + let firstLayerBrimIn = params.firstLayerBrimIn || 0; + let firstLayerBrimTrig = params.firstLayerBrimTrig || 0; + let firstLayerBrimGap = params.firstLayerBrimGap || 0; + let lastout, first = false; + let minz = Infinity, maxy = -Infinity, minx = Infinity, maxx = -Infinity; + let mins = Infinity; + let miny = Infinity; + let pads = []; + let overate = 0; + + for (let rec of layer) { + overate = rec.overate >= 0 ? rec.overate : overate; + let brate = params.firstLayerRate || firstLayerRate; + let bmult = params.firstLayerPrintMult || params.firstLayerPrintMult; + let point = rec.point; + let belty = rec.belty = -point.y + (point.z * belt.slope); + let lowrate = belty <= thresh ? brate : overate || brate; + miny = Math.min(miny, belty); + if (layer.anchor) { + // apply base rate to entire anchor (including bump) + rec.speed = Math.min(rec.speed, lowrate); + } + if (rec.emit && belty <= thresh && lastout && Math.abs(lastout.belty - belty) < 0.01) { + // apply base speed to segments touching belt + rec.speed = params.firstLayerRate || Math.min(rec.speed, lowrate); + rec.emit *= bmult; + rec.fan = params.firstLayerFanSpeed; + minx = Math.min(minx, point.x, lastout.point.x); + maxx = Math.max(maxx, point.x, lastout.point.x); + maxy = Math.max(maxy, point.y); + minz = Math.min(minz, point.z); + first = rec; + // find length of shortest bed-facing segment + mins = Math.min(mins, lastout.point.distTo2D(rec.point)); + // add to pads list if > 1mm long + if (point.x - lastout.point.x > 1) { + pads.push([lastout.point.x, point.x]); + } + } + lastout = rec; + } + // do not add brims to anchor layers + if (!first || layer.anchor) { + continue; + } + let tmpout = []; + let trigmet = firstLayerBrimTrig === 0 || (firstLayerBrimTrig && mins <= firstLayerBrimTrig); + let brimax = Math.max(firstLayerBrim, firstLayerBrimIn); + // add brim when all conditions met + if (brimax && trigmet) { + let { emit, tool } = first; + let y = maxy; + let z = minz; + let g = firstLayerBrimGap || 0; + let b = Math.max(firstLayerBrim, 1) + g; + let bi = Math.max(firstLayerBrimIn, 1) + g; + let lastLayerOut = layer.last(); + lastLayerOut.retract = true; + // if routaround selected, move to belt before moving to brim start + if (process.outputAvoidGaps) { + let beltPoint = lastLayerOut.point.clone(); + beltPoint.y = y; + print.addOutput(tmpout, beltPoint, 0, outputSeekrate, tool); + } + // outside brim + if (firstLayerBrim && !brimHalf) { + print.addOutput(tmpout, newPoint(maxx + b, y, z), 0, outputSeekrate, tool); + print.addOutput(tmpout, newPoint(maxx + g, y, z), emit, firstLayerRate, tool,{retract: true}) + } + // inside brim + if (firstLayerBrimIn && pads.length > 1) { + let gaps = []; + let lpad = pads[0]; + for (let pad of pads.slice(1)) { + let x0 = lpad[1]; + let x1 = pad[0]; + lpad = pad; + if (x1 - x0 > bi * 2) { + // over 2x brim so emit two segments + print.addOutput(tmpout, newPoint(x1 - g, y, z), 0, outputSeekrate, tool); + print.addOutput(tmpout, newPoint(x1 - bi, y, z), emit, firstLayerRate, tool, {retract: true}); + print.addOutput(tmpout, newPoint(x0 + bi, y, z), 0, outputSeekrate, tool); + print.addOutput(tmpout, newPoint(x0 + g, y, z), emit, firstLayerRate, tool, {retract: true}); + } else if (x1 - x0 > bi / 3) { + // over 1/3rd brim length emit single segment + print.addOutput(tmpout, newPoint(x1 - g, y, z), 0, outputSeekrate, tool); + print.addOutput(tmpout, newPoint(x0 + g, y, z), emit, firstLayerRate, tool, {retract: true}); + } + } + } + // outside brim + if (firstLayerBrim) { + print.addOutput(tmpout, newPoint(minx - b, y, z), 0, outputSeekrate, tool); + print.addOutput(tmpout, newPoint(minx - g, y, z), emit, firstLayerRate, tool, {retract: true}); + } + // when there is a printing move between layers + // inject non-printing move to after brim to prevent erroneous extrusion + if (layer[0].emit && lastLayerOut) { + print.addOutput(tmpout, layer[0].point.clone(), 0, outputSeekrate, tool); + } + layer.splice(0,0,...tmpout); + } else { + // for any layer touching belt, ensure start point is nearest origin + // print.addOutput(tmpout, newPoint(minx, maxy, minz), 0, outputSeekrate, first.tool); + // print.lastPoint = newPoint(minx, maxy, minz); + } + lastLayerOut = layer.peek(); + } + } + + // render if not explicitly disabled + if (settings.render !== false) { + await render.path(output, (progress, layer) => { + update(0.5 + progress * 0.5, "render", layer); + }, { + lineWidth: settings.process.sliceLineWidth, + toolMode: settings.pmode === 2, + tools: device.extruders, + thin: isThin, + flat: isFlat, + fdm: true + }); + } +}; + +function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { + const { settings } = print; + const { device } = settings; + const { bedWidth, bedDepth, bedRound } = device; + + let preout = [], + process = opt.params || settings.process, + originCenter = device.originCenter || bedRound, + extruder = parseInt(slice.extruder || 0), + nozzleSize = process.sliceLineWidth || device.extruders[extruder].extNozzle, + firstLayer = (opt.first || false) && !opt.support, + thinWall = nozzleSize * (opt.thinWall || 1.75), + retractDist = opt.retractOver || (nozzleSize * 5), + fillMult = opt.mult || process.outputFillMult, + shellMult = opt.mult || process.outputShellMult || (process.ctSliceHeight >= 0 ? 1 : 0), + shellOrder = {"out-in":-1,"in-out":1,"alternate":-2}[process.sliceShellOrder] || -1, + sparseMult = process.outputSparseMult, + coastDist = process.outputCoastDist || 0, + finishSpeed = opt.speed || process.outputFinishrate, + firstShellSpeed = process.firstLayerRate, + firstFillSpeed = process.firstLayerFillRate, + firstPrintMult = process.firstLayerPrintMult, + printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate), + fillSpeed = opt.speed || opt.fillSpeed || (firstLayer ? firstFillSpeed || firstShellSpeed : process.outputFeedrate), + infillSpeed = process.sliceFillRate || opt.infillSpeed || fillSpeed || printSpeed, + moveSpeed = process.outputSeekrate, + origin = startPoint.add(offset), + zhop = process.zHopDistance || 0, + antiBacklash = process.antiBacklash, + wipeDist = process.outputRetractWipe || 0, + isBelt = device.bedBelt, + bedOffset = originCenter ? { + x: 0, + y: 0, + z: 0 + } : { + x: bedWidth/2, + y: isBelt ? 0 : bedDepth/2, + z: 0 + }, + beltFirst = process.outputBeltFirst || false, + startClone = startPoint.clone(), + seedPoint = opt.seedPoint || startPoint, + switchTop = print.lastPoly && print.lastPoly.perimeter() < process.outputShortPoly, + z = slice.z, + lastPoly; + + // support alternating shell order + if (Math.abs(shellOrder) > 1 && slice.index % 2 === 1) { + shellOrder = -shellOrder; + } + + if (slice.finishSolids) { + fillSpeed = process.sliceSolidRate || finishSpeed; + } + + // apply first layer extrusion multipliers + if (firstLayer) { + fillMult *= firstPrintMult; + shellMult *= firstPrintMult; + sparseMult *= firstPrintMult; + } + + function retract() { + let array = preout.length ? preout : output; + if (array.length) { + let last = array.last(); + last.retract = true; + if (wipeDist && lastPoly && last.point) { + let center = lastPoly.center(true); + let maxDist = last.point.distTo2D(center); + let useDist = Math.min(wipeDist, maxDist); + let endpoint = last.point.followTo(center, useDist); + if (endpoint.inPolygon(lastPoly)) { + print.addOutput(array, endpoint, null, null, last.tool); + } + } + } else if (opt.pretract) { + opt.pretract(wipeDist); + } else { + console.log('unable to retract. no preout or output'); + } + } + + // return true if move path from p1 to p2 intersects a + // top and a path around the top (inside print) was not found + function intersectsTop(p1, p2) { + if (slice.index < 0) { + return false; + } + let int = false; + slice.topPolysFlat().forEach((poly) => { + if (!int) poly.forEachSegment((s1, s2) => { + if (util.intersect(p1,p2,s1,s2,base.key.SEGINT)) { + return int = true; + } + }); + }); + // if intersecting, look for a route around + if (int && opt.routeAround) { + return !routeAround(p1, p2); + } + return int; + } + + // returns true if routed around or no retract requried + function routeAround(p1, p2) { + const dbug = false; + if (dbug === slice.index) console.log(slice.index, {p1, p2, d: p1.distTo2D(p2)}); + + let ints = []; + let tops = slice.topRouteFlat(); + for (let poly of tops) { + poly.forEachSegment((s1, s2) => { + let ip = util.intersect(p1,p2,s1,s2,base.key.SEGINT); + if (ip) { + ints.push({ip, poly}); + } + }); + } + + // no intersections + if (ints.length === 0) { + if (dbug === slice.index) console.log(slice.index, 'no ints'); + return false; + } + + // odd # of intersections ?!? do retraction + if (ints.length && ints.length % 2 !== 0) { + if (dbug === slice.index) console.log(slice.index, {odd_intersects: ints}); + return false; + } + + // sort by distance + ints.sort((a, b) => { + return a.ip.dist - b.ip.dist; + }); + + if (dbug === slice.index) console.log(slice.index, {ints}); + + // check pairs. eliminate too close points. + // pairs must intersect same poly or retract. + for (let i=0; i i); + + if (ints.length > 2) { + if (dbug === slice.index) console.log(slice.index, {complex_route: ints.length}); + return false; + } + + if (ints.length === 2) { + // can route around intersected top polys + for (let i=0; ii.ip.dist), + i1, i2, same: i1.poly === i2.poly, + route, + p1, p2, dist: p1.distTo2D(p2), + r1, r1d, r1s, r1e, + r2, r2d, r2s, r2e, + isCW}); + + for (let p of route) { + print.addOutput(preout, p, 0, moveSpeed, extruder); + } + + // output last point + print.addOutput(preout, i2.ip, 0, moveSpeed, extruder); + } + return true; + } + + return false; + } + + // solid infill + function outputTraces(poly, opt = {}) { + if (!poly) return; + if (Array.isArray(poly)) { + if (opt.sort) { + let polys = poly.slice().sort((a,b) => { + return (a.perimeter() - b.perimeter()) * opt.sort; + }); + let debug = polys.length > 3; + let last; + while (polys.length) { + let next; + for (let p of polys) { + if (!last) { + next = p; + break; + } + if (opt.sort > 0) { + // in-out + if (last.isInside(p)) { + next = p; + break; + } + } else { + // out-in + if (p.isInside(last)) { + next = p; + break; + } + } + } + if (next) { + last = next; + polys.remove(next); + outputTraces(next, opt); + } else { + last = null; + } + } + } else { + outputOrderClosest(poly, function(next) { + outputTraces(next, opt); + }, null); + } + } else { + let finishShell = poly.depth === 0 && !firstLayer; + startPoint = print.polyPrintPath(poly, startPoint, preout, { + ccw: opt.shell && process.outputAlternating && slice.index % 2, + tool: extruder, + rate: finishShell ? finishSpeed : printSpeed, + accel: finishShell, + wipe: process.outputWipeDistance || 0, + coast: firstLayer ? 0 : coastDist, + extrude: numOrDefault(opt.extrude, shellMult), + onfirst: function(firstPoint) { + let from = seedPoint || startPoint; + if (from.distTo2D(firstPoint) > retractDist) { + if (intersectsTop(from, firstPoint)) { + retract(); + } + } + seedPoint = null; + } + }); + lastPoly = slice.lastPoly = poly; + } + } + + /** + * output sparse infill + * @param {Polygon[]} polys + */ + function outputSparse(polys, extrude, speed) { + if (!polys) return; + let proxy = polys.map((poly) => { + return {poly: poly, first: poly.first(), last: poly.last()}; + }); + let lp = startPoint; + startPoint = tip2tipEmit(proxy, startPoint, (el, point, count) => { + let poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + poly.forEachPoint((p, i) => { + let dist = lp.distTo2D(p); + let rdst = dist > retractDist; + let itop = rdst && intersectsTop(lp,p); + let emit = extrude; + // retract if dist trigger and crosses a slice top polygon + if (i === 0) { + if (itop) { + retract(); + emit = 0; + } else if (dist > nozzleSize) { + emit = 0; + } + } + // let emit = i === 0 ? 0 : extrude; + // handle shallow cloned infill + if (poly.z !== undefined) { + p = p.clone().setZ(poly.z); + } + print.addOutput(preout, p, emit, speed || printSpeed, extruder); + lp = p; + }, !poly.open); + return lp; + }); + } + + /** + * convert cross-hatch thin-fill into lines by connecting short segment mid-points + */ + function outputThin(lines) { + if (!lines) { + return; + } + let points = lines.group(2).map(grp => { + let [ p1, p2 ] = grp; + return newPoint( + (p1.x + p2.x) / 2, + (p1.y + p2.y) / 2, + (p1.z + p2.z) / 2 + ) + }); + let order = util.orderClosest(points, (p1, p2) => p1.distTo2D(p2)); + if (order.length === 0) { + return; + } + let first = points[0]; + let last = first; + if (startPoint && startPoint.distTo2D(first) > thinWall && intersectsTop(startPoint, first)) { + retract(); + } + print.addOutput(preout, first, 0, moveSpeed, extruder); + for (let p of order) { + let dist = last ? last.distTo2D(p) : 0; + if (dist > thinWall) { + retract(); + print.addOutput(preout, p, 0, moveSpeed, extruder); + } + print.addOutput(preout, p, 1, fillSpeed, extruder); + startPoint = last = p; + } + // close a circle + if (last && last.distTo2D(first) <= thinWall) { + print.addOutput(preout, first, 1, fillSpeed, extruder); + startPoint = first; + } + } + + /** + * output solid infill + * @param {*} lines + * @param {*} opt + */ + function outputFills(lines, opt = {}) { + if (!lines || lines.length === 0) { + return; + } + let p, p1, p2, dist, len, found, group, mindist, t1, t2, + marked = 0, + start = 0, + skip = false, + lastIndex = -1, + raft = opt.raft || false, + flow = opt.flow || 1, + near = opt.near || (antiBacklash ? false : true), + fast = opt.fast || false, // support infill only! + fill = (opt.fill >= 0 ? opt.fill : fillMult) * flow, + thinDist = near ? thinWall : thinWall; + + // continue until all lines in array are "marked" as used + while (lines && marked < lines.length) { + group = null; + found = false; + mindist = Infinity; + + // use next nearest line endpoint strategy + if (near) + for (let i=0; i lastIndex) { + group = p.index; + } + if (group !== null) { + if (p.index !== group) { + break; + } + if (p.index % 2 === 0) { + t1 = lines[i]; + t2 = lines[i+1]; + } else { + t2 = lines[i]; + t1 = lines[i+1]; + } + dist = Math.min(t1.distTo2D(startPoint), t2.distTo2D(startPoint)); + if (dist < mindist) { + p1 = t1; + p2 = t2; + mindist = dist; + } + start = i; + found = true; + } + } + + // go back to start and try again + if (!near && !found) { + if (start === 0 && lastIndex === -1) { + console.log('infinite loop', lines, { + marked, i, group, start, lastIndex, + points: lines.map(p => p.index).join(', ') + }); + break; + } + start = 0; + lastIndex = -1; + continue; + } + + dist = startPoint.distToSq2D(p1); + len = p1.distToSq2D(p2); + + // go back to start when dist > retractDist + if (!near && !fast && !skip && dist > retractDist) { + skip = true; + start = 0; + lastIndex = -1; + continue; + } + skip = false; + + // re-seek a new start index within fill array + let restart = lastIndex < 0; + + // mark as used + p1.del = true; + p2.del = true; + marked += 2; + lastIndex = p1.index; + + // if dist to new segment is less than thinWall + // and segment length is less than thinWall then + // just extrude to midpoint of next segment. this is + // to avoid shaking the printer to death. + if (dist <= thinDist && len <= thinDist) { + p2 = p1.midPointTo(p2); + // this.addOutput(preout, p2, fill * (dist / thinWall), fillSpeed, extruder); + print.addOutput(preout, p2, fill, fillSpeed, extruder); + } else { + // retract if dist trigger or crosses a slice top polygon + if (!fast && dist > retractDist && (zhop || intersectsTop(startPoint, p1))) { + retract(); + } + + // anti-backlash on longer move + if (!fast && antiBacklash && dist > retractDist) { + print.addOutput(preout, p1.add({x:antiBacklash,y:-antiBacklash,z:0}), 0, moveSpeed, extruder); + } + + // bridge ends of fill when they're close together + if (dist < thinDist) { + print.addOutput(preout, p1, fill, fillSpeed, extruder); + } else if (raft && !restart) { + // connect raft lines unless it's a restart + print.addOutput(preout, p1, fill, fillSpeed, extruder); + } else { + print.addOutput(preout, p1, 0, moveSpeed, extruder); + } + print.addOutput(preout, p2, fill, fillSpeed, extruder); + } + + startPoint = p2; + } + + // clear delete marks so we can re-print later + if (lines) lines.forEach(p => { p.del = false }); + } + + /** + * given array of polygons, emit them in next closest order with + * the special exception that depth is considered into distance + * so that inner polygons are emitted first. + * + * @param {Array} array of Polygons or Polygon wrappers (tops) + * @param {Function} fn call to emit next candidate + * @param {Function} fnp convert 'next' object into a Polygon for closeness + */ + function outputOrderClosest(array, fn, fnp) { + if (array.length === 1) { + return fn(array[0]); + } + array = array.slice(); + let closest, find, next, order, poly, lastDepth = 0; + for (;;) { + order = []; + closest = null; + for (let i=0; i { + return b.d - a.d; + }); + } else { + order.sort((a,b) => { + return a.d - b.d; + }); + } + array[order[0].i] = null; + fn(order[0].n); + } + } + + let out = []; + if (slice.tops) { + out.appendAll(slice.tops); + } + if (opt.support && slice.supports) { + out.appendAll(slice.supports); + } + + let lastTop = null; + outputOrderClosest(out, function(next) { + if (next instanceof Polygon) { + print.setType('support'); + // support polygon + next.setZ(z); + if (process.sliceSupportOutline !== false) { + outputTraces([next].appendAll(next.inner || [])); + } + if (next.fill) { + next.fill.forEach(p => { p.z = z }); + outputFills(next.fill, {fast: true, near: true}); + } + } else { + // solid infill + let top = next; + let isRaft = top.isRaft; + + print.setType('shells'); + if (lastTop && lastTop !== next) { + retract(); + } + + // control of layer start point + switch (process.sliceLayerStart) { + case "last": + break; + case "random": + let bounds = top.poly.bounds; + let center = bounds.center(); + let radius = Math.max(bounds.width(), bounds.height()); + let angle = Math.random()*Math.PI*2; + let x = center.x + Math.cos(angle)*radius; + let y = center.y + Math.sin(angle)*radius; + startPoint = newPoint(x,y,startPoint.z); + break; + case "center": + startPoint = newPoint(0,0,startPoint.z); + break; + case "origin": + startPoint = newPoint(-bedOffset.x, -bedOffset.y, startPoint.z); + break; + case "position": + startPoint = newPoint(-bedOffset.x+process.sliceLayerStartX, -bedOffset.y+process.sliceLayerStartY, startPoint.z); + break; + } + + // optimize start point on belt for tops touching belt + // and enforce optimal shell order (outer first) + if (isBelt && opt.onBelt) { + startPoint = startClone; + if (beltFirst) { + shellOrder = -1; + } + } + + // raft + if (top.traces) outputTraces(top.traces); + + // innermost shells + let inner = next.innerShells() || []; + let shells = next.shells || []; + + // alternating winding option + // if (process.outputAlternating || slice.index % 2) { + // console.log({alternating: slice, shells, inner}); + // POLY.setWinding(inner, false); + // POLY.setWinding(shells, false); + // } + + // output inner polygons + if (shellOrder > 0) outputTraces(inner, { sort: shellOrder, shell: true }); + + outputTraces(shells, { sort: shellOrder, shell: true }); + + // output outer polygons + if (shellOrder < 0) outputTraces(inner, { sort: shellOrder, shell: true }); + + // output thin fill + print.setType('thin fill'); + outputThin(next.thin_fill); + + // then output solid and sparse fill + print.setType('solid fill'); + outputFills(next.fill_lines, { flow: fillMult, raft: isRaft }); + + print.setType('sparse infill'); + outputSparse(next.fill_sparse, sparseMult, infillSpeed); + + lastTop = next; + } + }, function(obj) { + // for tops, return the wrapped poly + return obj instanceof Polygon ? obj : obj.poly; + }); + + // produce polishing paths when present + if (slice.tops && slice.tops.length && slice.tops[0].polish) { + let {x,y} = slice.tops[0].polish; + if (x) { + outputSparse(x, 0, process.polishSpeed); + } + if (y) { + outputSparse(y, 0, process.polishSpeed); + } + } + + // offset print points + for (let i=0; i !w.meta.disabled) + .sort((a,b) => { + return a.slices[0].z - b.slices[0].z + }); + // assign grid_id which can be embedded in gcode and + // used by the controller to cancel objects during print + let const { bounds } = settings;;for (let widget of widgets) const { let { pos, box } = widget.track;;// calculate top/left coordinate for widget + // relative to bounding box for all widgets + let tl = { + x: Math.round((pos.x - box.w/2 - bounds.min.x) / 10) + 1, + y: Math.round((pos.y - box.h/2 - bounds.min.y) / 10) + 1 + }; + widget.track.grid_id = tl.x * 100 + tl.y; + } + // count extruders used + let ext = []; + for (let w of widgets) { + if (w.anno && w.anno.extruder >= 0) { + let e = w.anno.extruder; + if (ext.indexOf(e) < 0) { + ext.push(e); + } + } + } + // sort widgets by first slice Z + widgets.sort((a,b) => { + return a.slices[0].z - b.slices[0].z; + }); + // give first widget a pass since it should have the anchor + widgets.shift(); + // remove anchor slices from other widgets (only with multi-material) + if (ext.length > 1) { + for (let w of widgets) { + w.slices = w.slices.filter(s => s.index >= 0); + } + } +}; + +/** + * DRIVER SLICE CONTRACT + * + * Given a widget and settings object, call functions necessary to produce + * slices and then the computations using those slices. This function is + * designed to run client or server-side and provides all output via + * callback functions. + * + * @param {Object} settings + * @param {Widget} Widget + * @param {Function} onupdate (called with % complete and optional message) + * @param {Function} ondone (called when complete with an array of Slice objects) + */ +FDM.slice = function(settings, widget, onupdate, ondone) const { let render = settings.render !== false, { process, device, controller } = settings;isBelt = device.bedBelt, + isBrick = controller.devel && process.sliceZInterleave, + isSynth = widget.track.synth, + isSupport = widget.track.support, + useAssembly = controller.assembly, + isConcurrent = controller.threaded && kiri.minions.concurrent, + topLayers = process.sliceTopLayers || 0, + bottomLayers = process.sliceBottomLayers || 0, + vaseMode = process.sliceFillType === 'vase' && !isSynth, + metadata = widget.anno, + maxtruder = Math.max(0, device.extruders.length - 1), + extruder = Math.min(maxtruder, parseInt(isSynth ? process.sliceSupportNozzle : metadata.extruder || 0)), + sliceHeight = process.sliceHeight, + sliceHeightBase = (isBelt ? sliceHeight : process.firstSliceHeight) || sliceHeight, + lineWidth = process.sliceLineWidth || device.extruders[extruder].extNozzle, + fillOffsetMult = 1.0 - bound(process.sliceFillOverlap, 0, 0.8), + shellOffset = lineWidth, + fillSpacing = lineWidth, + fillOffset = lineWidth * fillOffsetMult, + clipOffset = process.sliceSupportOffset, + sliceFillAngle = process.sliceFillAngle, + supportDensity = process.sliceSupportDensity; + + // override globals used by vopt() + isFlat = controller.lineType === "flat"; + isThin = !isFlat && controller.lineType === "line"; + offset = lineWidth / 2; + + // allow overriding support fill auto angle algorithm + // also causes support fill to be aligned on start boundaries + // best with angles that are a multiple of 90 degrees + if (process.sliceSupportFill >= 0) { + // yes, an ugly hack to allow it to pass through old code paths + process.sliceSupportFill += 1000; + } + + if (isFlat) { + Object.values(COLOR).forEach(color => { + color.flat = true; + color.line = 1 + color.opacity = 1; + }); + } else { + Object.keys(COLOR).forEach(key => { + const color = COLOR[key]; + const proto = PROTO[key] + color.flat = proto.flat; + color.line = proto.line; + color.opacity = proto.opacity; + }); + } + + if (!(sliceHeight > 0 && sliceHeight < 100)) { + return ondone("invalid slice height"); + } + if (!(lineWidth >= 0.01 && lineWidth < 100)) { + return ondone("invalid nozzle size"); + } + + const sliceMinHeight = process.sliceAdaptive && process.sliceMinHeight > 0 ? + Math.min(process.sliceMinHeight, sliceHeight) : 0; + + if (sliceHeightBase <= 0) { + console.log("invalid first layer height < slice height"); + console.log("reverting to min valid slice height"); + sliceHeightBase = sliceMinHeight || sliceHeight; + } + + let bounds = widget.getBoundingBox(); + let points = widget.getPoints(); + let indices = []; + let heights = []; + let healed = false; + + // handle z cutting (floor method) and base flattening + let zPress = isBelt ? process.firstLayerFlatten || 0 : 0; + let zCut = widget.track.zcut || 0; + let const { belt } = widget;;if (zCut || zPress) { + for (let p of points) { + if (!p._z) { + p._z = p.z; + if (zPress) { + if (isBelt) { + let zd = (belt.slope * p.z) - p.y; + if (zd > 0 && zd <= zPress) { + p.y += zd * belt.cosf; + p.z -= zd * belt.sinf; + } + } else { + if (p.z <= zPress) p.z = 0; + } + } + if (zCut && !isBelt) { + p.z -= zCut; + } + } + } + } + + base.slice(points, { + debug: process.xray, + xray: process.xray, + zMin: bounds.min.z, + zMax: bounds.max.z - zCut, + // support/synth usually has overlapping boxes + union: controller.healMesh || isSynth, + indices: process.indices || process.xray, + useAssembly, + post: 'FDM', + post_args: { + shellOffset, + fillOffset, + clipOffset, + lineWidth, + vaseMode, + isSynth, + process, + }, + // z index generator + zGen(zopt) { + if (process.xray) { + return zopt.zIndexes; + } + let const { zMin, zMax } = zopt;;let h1 = sliceHeight; + let h0 = sliceHeightBase || h1; + let hm = sliceMinHeight || 0; + let h = h0; + let z = h0; + let zi = indices; // indices + let zh = heights; // heights + if (hm) { + // adaptive increments based on z indices (var map to legacy code) + let zIncFirst = h0; + let zInc = h1; + let zIncMin = hm; + let zHeights = heights; + let zIndexes = indices; + let zOrdered = Object.values(zopt.zIndexes).map(v => parseFloat(v)); + // console.log('adaptive slicing', zIncMin, ':', zInc, 'from', zMin, 'to', zMax); + let zPos = zIncFirst, + zOI = 0, + zDelta, + zDivMin, + zDivMax, + zStep, + nextZ, + lzp = zPos; + // adaptive slice height + // first slice/height is fixed from base + zHeights.push(zIncFirst); + zIndexes.push(zIncFirst); + // console.log({zIncFirst, zOrdered}) + while (zPos < zMax && zOI < zOrdered.length) { + nextZ = zOrdered[zOI++]; + if (zPos >= nextZ) { + // console.log('skip',{zPos},'>=',{nextZ}); + continue; + } + zDelta = nextZ - zPos; + if (zDelta < zIncMin) { + // console.log('skip',{zDelta},'<',{zIncMin}); + continue; + } + zDivMin = Math.floor(zDelta / zIncMin); + zDivMax = Math.floor(zDelta / zInc); + if (zDivMax && zDivMax <= zDivMin) { + if (zDelta % zInc > 0.01) zDivMax++; + zStep = zDelta / zDivMax; + // console.log(`--- zDivMax <= zDivMin ---`, zStep, zDelta % zInc) + } else { + zStep = zDelta; + } + // console.log({nextZ, zPos, zDelta, zStep, zDivMin, zDivMax}) + while (zPos < nextZ) { + zHeights.push(zStep); + zIndexes.push(zPos + zStep); + zPos += zStep; + // console.log({D: zPos - lzp, zPos}) + // lzp = zPos; + } + } + // console.log({zIndexes, zHeights}); + } else { + // simple based + fixed increment + while (true) { + // reduce slice position by half layer height + let realz = (z - (h / 2)).round(3); + if (realz > zMax) { + break; + } + zh.push(h); + zi.push(realz); + h = h1; + z += h; + } + } + return zi; + }, + // slicer function (worker local or minion distributed) + slicer(z, points, opts) { + // opts.debug = opts.debug || isSynth; + return (isConcurrent ? kiri.minions.sliceZ : base.sliceZ)(z, points, opts); + }, + onupdate(update) { + return onupdate(0.0 + update * 0.5) + } + }).then((output) => const { // post process slices and re-incorporate missing meta-data + return output.slices.map(data => { + let { z, clip, lines, groups, changes } = data;;if (!data.tops) return null; + let slice = newSlice(z).addTops(data.tops); + slice.index = indices.indexOf(z); + slice.height = heights[slice.index]; + slice.clips = clip; + // do not warn on merging supports + if (changes && !isSynth) { + healed = true; + slice.changes = changes; + if (self.debug) { + console.log('slice healed', slice.index, slice.z, changes); + } + } + if (process.xray) { + slice.index = process.xrayi.shift(); + slice.lines = lines; + slice.groups = groups; + slice.xray = process.xray; + } + return slice; + }).filter(s => s); + }).then(slices => { + return onSliceDone(slices); + }).then(ondone); + + // shadow used to clip supports in non-belt mode + async function doShadow(slices) { + if (widget.shadow) { + return; + } + let root = widget.group[0]; + if (root.shadow) { + widget.shadow = root.shadow; + return; + } + // console.log({ doShadow: widget, slices }); + // create shadow for clipping supports + let alltops = widget.group + .filter(w => !w.track.synth) // no supports in shadow + .map(w => w.slices).flat() + .map(s => s.tops).flat().map(t => t.simple); + let shadow = isConcurrent ? + await kiri.minions.union(alltops, 0.1) : + POLY.union(alltops, 0.1, true); + // expand shadow when requested (support clipping) + if (process.sliceSupportExtra) { + shadow = POLY.offset(shadow, process.sliceSupportExtra); + } + widget.shadow = root.shadow = POLY.setZ(shadow, 0); + // slices[0].output() + // .setLayer('shadow', { line: 0xff0000, check: 0xff0000 }) + // .addPolys(shadow); + } + + async function onSliceDone(slices) { + // alert non-manifold parts + if (healed) { + onupdate(null, null, "part may not be manifold"); + } + + // remove all empty slices above part but leave below + // for multi-part (multi-extruder) setups where the void is ok + // also reverse because slicing occurs bottom-up + let found = false; + slices = slices.reverse().filter(slice => { + if (slice.tops.length) { + return found = true; + } else { + return found; + } + }).reverse(); + + // connect slices into linked list for island/bridge projections + for (let i=1; i 0.1) { + continue; + } + let clipto = pslice.clips; + if (pslice.supportOutline) { + // merge support outlines into slice clips which + // should only happen when automatic and manual + // supports are used together + clipto.appendAll(pslice.supportOutline); + } + let ntops = []; + POLY.subtract(tops, clipto, ntops, null, slice.z, 0); + tops = ntops; + } + // trim to group's shadow if not in belt mode + if (!isBelt) { + tops = POLY.setZ(POLY.trimTo(tops, widget.shadow), slice.z); + } + } + slice.tops = []; + for (let t of tops) { + slice.addTop(t); + } + doShells(slice, 1, shellOffset / 2); + } + } + + // calculate % complete and call onupdate() + function doupdate(index, from, to, msg) { + trackupdate(index / slices.length, from, to, msg); + } + + function trackupdate(pct, from, to, msg) { + onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg); + } + + // for each slice, performe a function and call doupdate() + function forSlices(from, to, fn, msg) { + slices.forEach(slice => { + fn(slice); + doupdate(slice.index, from, to, msg) + }); + } + + // do not hint polygon fill longer than a max span length + config.hint_len_max = util.sqr(process.sliceBridgeMax); + + // reset for solids, support projections + // and other annotations + slices.forEach(slice => { + slice.widget = widget; + slice.extruder = extruder; + slice.solids = []; + }); + + // just the top/bottom special solid layers or range defined solid layers + forSlices(0.15, 0.2, slice => { + let range = slice.params; + let spaceMult = slice.index === 0 ? process.firstLayerLineMult || 1 : 1; + let isBottom = slice.index < bottomLayers; + let isTop = topLayers && slice.index > slices.length - topLayers - 1; + let isDense = range.sliceFillSparse > 0.995; + let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; + let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; + if (solidWidth) { + let fillSpace = fillSpacing * spaceMult * solidWidth; + doSolidLayerFill(slice, fillSpace, sliceFillAngle); + } + sliceFillAngle = (sliceFillAngle + 90.0) % 360; + }, "solid layers"); + + // add lead in anchor when specified in belt mode (but not for synths) + if (isBelt && !isSynth) const { let { cosf, slope } = widget.belt;;// find adjusted zero point from slices + let smin = Infinity; + for (let slice of slices) { + let miny = Infinity; + for (let poly of slice.topPolys()) { + let y = poly.bounds.maxy; + let z = slice.z; + // at 45 degrees, 1mm in Z is 1mm in Y + let by = (slope * z) - y; + if (by < miny) miny = by; + if (by < smin) smin = by; + } + // mark slices with tops touching belt + slice.belt = { miny, touch: miny.round(3) < sliceHeightBase }; + } + // find max width of first 5 layers for brim additions + let start; + let minx = Infinity, maxx = -Infinity; + let peek = 0; + for (let slice of slices) { + if (slice.tops.length && peek++ < 5) { + for (let poly of slice.topPolys()) { + minx = Math.min(minx, poly.bounds.minx); + maxx = Math.max(maxx, poly.bounds.maxx); + } + } + // find first slice touching belt for start of anchor + if (!start && slice.belt.touch) { + start = slice; + } + } + // ensure we start against a layer with shells + while (start && start.up && start.topShells().length === 0) { + start = start.up; + } + // if a brim applies, add that width to anchor + let brim = getRangeParameters(process, 0).firstLayerBrim || 0; + if (brim) { + minx -= brim; + maxx += brim; + } + // array of added top.fill_sparse arrays + let adds = []; + let step = sliceHeight; + let anchorlen = (process.beltAnchor || process.firstLayerBeltLead) * cosf; + while (anchorlen && start && anchorlen >= sliceHeight) { + let addto = start.down; + if (!addto) { + addto = newSlice(start.z - step); + addto.extruder = extruder; + addto.belt = { }; + addto.height = start.height; + addto.up = start; + start.down = addto; + slices.splice(0,0,addto); + } else if (!addto.belt) { + console.log({addto_missing_belt: addto}); + addto.belt = {}; + } + addto.index = -1; + addto.belt.anchor = true; + // this allows the anchor to print bi-directionally + // by removing the forced start-point in print.js + addto.belt.touch = false; + let z = addto.z; + let y = (slope * z) - smin - (lineWidth / 2); + let splat = base.newPolygon().add(minx, y, z).add(maxx, y, z).setOpen(); + let snew = addto.addTop(splat).fill_sparse = [ splat ]; + adds.push(snew); + start = addto; + anchorlen -= (step * slope); + } + // add anchor bump + let bump = process.firstLayerBeltBump; + if (bump) { + adds = adds.reverse().slice(1, adds.length - 1); + let count = 1; + for (let add of adds) { + let poly = add[0]; + let y = count++ * -start.height * 2; + if (-y > bump) { + count--; + // break; + } + let first = poly.first(); + // add up/over/down to anchor line (close = down) + // which completes the bump perimeter + poly.push(poly.last().add({x:0, y, z:0})); + poly.push(poly.first().add({x:0, y, z:0})); + poly.setClosed(); + if (count > 2 && maxx - minx > 10) { + // add vertical hatch lines insibe bump shell + let mp = (maxx + minx) / 2; + let dx = (maxx - minx - 2); + dx = (Math.floor(dx / 3) * 3) / 2; + let fy = first.y; + let fz = first.z; + let n2 = lineWidth / 2; + for (let x = mp - dx; x <= mp + dx ; x += 3) { + add.push( base.newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() ); + } + } + } + } + // experimental emboss text on a flat underside of an object + // in belt mode only + if (process.pooch && self.OffscreenCanvas) const { const { length, width, height, text } = process.pooch;;let firstZ, firstI, lastZ, lastI, minX = 0, maxX = 0, maxY = 0; + // locate suitable flat spot + for (let slice of slices) const { let { belt } = slice;;if (!belt.touch) { + continue; + } + let index = slice.index; + let z = slice.z; + outer: for (let poly of slice.topPolys()) { + for (let i=0, p=poly.points, l=p.length; i 0 && p0y < 3 && Math.abs(p0y - p1y) < 0.01; + let x_ok = Math.abs(width - Math.abs(p1.x - p0.x)) < 1 + if (y_ok && x_ok) { + if (i_ok) { + firstZ = firstZ || z; + firstI = firstI || index; + lastZ = z; + lastI = index; + minX = Math.min(p0.x, p1.x); + maxX = Math.max(p0.x, p1.x); + maxY = p0y; + break outer; + } else { + firstZ = lastI = 0; + } + } + } + } + } + let dy = Math.abs(height - maxY * (1 / Math.sqrt(2))); + let dz = Math.abs(length - ((lastZ - firstZ) * Math.sqrt(2))); + if (dy < 0.1 && dz < 1) { + // console.log('FOUND', { firstI, lastI, minX, maxX, maxY }); + let span = lastI - firstI - 2; // x = down the belt + let tall = width * 2; + let can = new self.OffscreenCanvas(span, tall); + let ctx = can.getContext("2d"); + ctx.scale(1.2, 1); + ctx.font = '24px sans-serif'; + // ctx.fillStyle = 'black'; + ctx.textBaseline = "bottom"; + ctx.fillText(text, 1, tall - 1); + let img = ctx.getImageData(0, 0, span, tall).data.buffer; + let rgb = new Uint32Array(img); + // console.log({ img, rgb, p: rgb.filter(v => v) }); + for (let x=0; x=0; y--) { + let pix = rgb[y * span + x]; + pix = ( + ((pix >> 24) & 0xff) + + ((pix >> 16) & 0xff) + + ((pix >> 8) & 0xff) + ) / 3; + str += pix > 30 ? '*' : '-'; + maxp = Math.max(maxp, pix); + if (pix > 30) { + if (start >= 0) { + end = tall - y; + } else { + start = tall - y; + } + } else { + if (start >= 0 && end > start) { + lines.push({ start, end }); + } + start = end = undefined; + } + } + console.log((x).toString().padStart(2,0),str,maxp | 0,lines); + if (lines.length) { + let slice = slices[firstI + x + 1]; + let supps = slice.supports = slice.supports || []; + let z = slice.z; + let y = z - smin - (lineWidth / 2); + for (let line of lines) { + supps.push(base.newPolygon() + .add(minX + line.start / 2, y, z) + .add(minX + line.end / 2, y, z) + .setOpen() + ); + } + } + } + } + } + } + + // calculations only relevant when solid layers are used + // layer boolean diffs need to be computed to find flat areas to fill + // and overhangs that need to be supported. these are stored in flats + // and bridges, projected up/down, and merged into an array of solids + if (!vaseMode && !isSynth) { + profileStart("delta"); + forSlices(0.2, 0.34, slice => { + let params = slice.params || process; + let solidMinArea = params.sliceSolidMinArea; + let sliceFillGrow = params.sliceFillGrow; + doDiff(slice, { min: solidMinArea, grow: sliceFillGrow }); + }, "layer deltas"); + profileEnd(); + profileStart("delta-project"); + forSlices(0.34, 0.35, slice => { + let params = slice.params || process; + topLayers = params.sliceTopLayers || 0; + bottomLayers = params.sliceBottomLayers || 0; + if (topLayers) projectFlats(slice, topLayers); + if (bottomLayers) projectBridges(slice, bottomLayers); + }, "layer deltas"); + profileEnd(); + profileStart("solid-fill") + let promises = isConcurrent ? [] : undefined; + forSlices(0.35, promises ? 0.4 : 0.5, slice => { + let params = slice.params || process; + let first = slice.index === 0; + let solidWidth = params.sliceFillWidth || 1; + let spaceMult = first ? params.firstLayerLineMult || 1 : 1; + let fillSpace = fillSpacing * spaceMult * solidWidth; + let solidMinArea = params.sliceSolidMinArea; + doSolidsFill(slice, fillSpace, sliceFillAngle, solidMinArea, promises); + sliceFillAngle = (sliceFillAngle + 90.0) % 360; + }, "fill solids"); + // very last layer (top) is set to finish solid rate + slices.last().finishSolids = true + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.4, 0.5); + }); + } + profileEnd(); + } + + // for "real" objects, fill the remaining voids with sparse fill + // sparse layers only present when non-vase mose and sparse % > 0 + if (!isSynth && !vaseMode) { + let lastType; + let promises = isConcurrent ? [] : undefined; + forSlices(0.5, promises ? 0.55 : 0.7, slice => { + let params = slice.params || process; + if (!params.sliceFillSparse) { + return; + } + let newType = params.sliceFillType; + doSparseLayerFill(slice, { + settings, + process, + device, + lineWidth, + spacing: fillOffset, + density: params.sliceFillSparse, + bounds: widget.getBoundingBox(), + height: sliceHeight, + type: newType, + cache: params._range !== true && lastType === newType, + promises + }); + lastType = newType; + }, "infill"); + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.55, 0.7); + }); + } + // back-fill slices marked for infill cloning + for (let slice of slices) { + if (slice._clone_sparse) { + let tops = slice.tops; + let down = slice.down.tops; + for (let i=0; i p.cloneZ(slice.z)); + } + } + } + } else if (isSynth && isSupport) { + // convert synth support widgets into support structure + let outline = process.sliceSupportOutline || false; + let promises = isConcurrent ? [] : undefined; + let resolve = []; + forSlices(0.5, promises ? 0.6 : 0.7, slice => { + let params = slice.params || process; + let density = params.sliceSupportDensity; + let supports = slice.topShells(); + slice.supports = supports; + slice.tops = undefined; // remove outline leave only supports + let polys = []; + if (density) { + if (!outline) { + POLY.expand(supports, lineWidth, slice.z, polys); + } else { + POLY.expand(supports, -lineWidth/4, slice.z, polys); + } + } + fillSupportPolys({ + promises, polys, lineWidth, density, z: slice.z, isBelt, + angle: process.sliceSupportFill + }); + resolve.push({ slice, polys }); + }, "infill"); + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.6, 0.7); + }); + } + for (let rec of resolve) const { let { slice, polys } = rec;;rec.supports = polys; + } + } + + // auto support generation + if (!isBelt && !isSynth && supportDensity && process.sliceSupportEnable) { + doShadow(slices); + profileStart("support"); + let promises = []; + forSlices(0.7, 0.75, slice => { + promises.push(doSupport(slice, process, widget.shadow, { })); + }, "support"); + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.75, 0.8); + }); + profileEnd(); + } + + // fill all supports (auto and manual) + // if (!isBelt && supportDensity) { + if (supportDensity) { + profileStart("support-fill"); + let promises = false && isConcurrent ? [] : undefined; + forSlices(0.8, promises ? 0.88 : 0.9, slice => { + let params = slice.params || process; + let density = params.sliceSupportDensity; + doSupportFill({ + promises, slice, lineWidth, density, + minArea: process.sliceSupportArea, isBelt, + angle: process.sliceSupportFill, + outline: process.sliceSupportOutline !== false + }); + }, "support"); + if (promises) { + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.88, 0.9); + }); + } + profileEnd(); + } + + if (isBrick) { + let indices = slices.map(s => s.index); + let first = indices[1]; + let last = indices[indices.length - 2]; + let nu = []; + for (let slice of slices) { + if (slice.index < first || slice.index > last) { + continue; + } + let nuSlice = slice.clone(); + nuSlice.z -= slice.height / 2; + if (slice.index === first) { + nuSlice.z = slice.z - slice.height / 4; + nuSlice.height = slice.height / 2; + } else { + nuSlice.height = slice.height; + } + nu.push(nuSlice); + let ti = 0; + for (let top of slice.tops || []) { + let nuTop = nuSlice.tops[ti++]; + nuTop.shells = []; + top.shells = top.shells.filter((s,i) => { + if (i % 2 === 0) { + return true; + } else { + nuTop.shells.push(s); + return false; + } + }); + } + if (slice.index === last) { + let cap = nuSlice.clone(); + cap.z += (slice.height * 0.75); + cap.height = (slice.height / 2); + nu.push(cap); + cap.tops?.forEach((top, i) => { + top.shells = nuSlice.tops[i].shells.clone(); + }); + } + } + slices.appendAll(nu); + slices.sort((a,b) => a.z - b.z); + slices.forEach((s,i) => s.index = i); + } + + // render if not explicitly disabled + if (render) { + forSlices(0.9, 1.0, slice => { + let params = slice.params || process; + doRender(slice, isSynth, params, { + dark: controller.dark, + devel: controller.devel + }); + }, "render"); + } + + if (isBelt) { + let bounds = base.newBounds(); + let slice = slices[0]; + if (slice.tops) { + for (let top of slice.tops) { + bounds.merge(top.poly.bounds); + } + } else if (slice.supports) { + for (let poly of slice.supports) { + bounds.merge(poly); + } + } + widget.belt.miny = -bounds.miny; + widget.belt.midy = (bounds.miny + bounds.maxy) / 2; + } + } + +} + +function connect_lines(lines, maxd = Infinity) { + const newlines = []; + let op2; + let eo = 0; + for (let i=0; i { + if (isThin) output + .setLayer('part', Color.part) + .addPolys([top.poly]); + + output + .setLayer(isSynth ? "support" : "shells", isSynth ? Color.support : Color.shell) + .addPolys(top.shells || [], vopt({ offset, height, clean: true })); + + output + .setLayer("solid fill", isSynth ? Color.support : Color.fill) + .addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height, z:slice.z })); + + if (!(slice.belt && slice.belt.anchor)) output + .setLayer("sparse fill", Color.infill) + .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) + + if (slice.belt && slice.belt.anchor) output + .setLayer("anchor", Color.anchor) + .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) + + if (top.thin_fill) output + .setLayer("thin fill", Color.thin) + .addLines(top.thin_fill, vopt({ offset, height })); + + if (top.gaps && devel) output + .setLayer("gaps", Color.gaps) + .addPolys(top.gaps, vopt({ offset, height, thin: true })); + + if (isThin && devel && top.fill_off && top.fill_off.length) { + slice.output() + .setLayer('fill inset', Color.inset) + .addPolys(top.fill_off); + // .setLayer('last', { face: 0, line: 0x008888, check: 0x008888 }) + // .addPolys(top.last); + } + }); + + if (isThin && devel) { + if (slice.solids && slice.solids.length) output + .setLayer("solids", { face: 0xbbbb00, check: 0xbbbb00 }) + .addAreas(slice.solids); + + if (slice.bridges && slice.bridges.length) output + .setLayer("bridges", { face: 0x00cccc, line: 0x00cccc, check: 0x00cccc }) + .addAreas(slice.bridges); + + if (slice.flats && slice.flats.length) output + .setLayer("flats", { face: 0xaa00aa, line: 0xaa00aa, check: 0xaa00aa }) + .addAreas(slice.flats); + } + + if (slice.supports && params.sliceSupportOutline) output + .setLayer("support", Color.support) + .addPolys(slice.supports, vopt({ offset, height })); + + if (slice.supports) slice.supports.forEach(poly => { + if (poly.fill) output + .setLayer("support", Color.support) + .addLines(poly.fill, vopt({ offset, height })); + }); + + if (slice.xray) { + const color = [ 0xff0000, 0x00aa00, 0x0000ff, 0xaaaa00, 0xff00ff, 0x0 ]; + if (slice.lines) { + slice.lines.forEach((line, i) => { + const group = i % 5; + slice.output().setLayer(`l${group}`, color[group]).addLine(line.p1, line.p2); + }); + } + if (slice.groups) + POLY.nest(slice.groups).forEach((poly, i) => { + const group = i % 5; + slice.addTop(poly); + // slice.output().setLayer(`g${i}`, 0x888888).addPoly(poly); + slice.output().setLayer(`g${i}`, color[group]).addPoly(poly); + }); + } + + // console.log(slice.index, slice.render.stats); +} + +// shared with SLA driver and minions +FDM.share = { + doShells, + doTopShells, + doDiff, + projectFlats, + projectBridges +}; + +/** + * Compute offset shell polygons. For FDM, the first offset is usually half + * of the nozzle width. Each subsequent offset is a full nozzle width. User + * parameters control tweaks to these numbers to allow for better shell bonding. + * The last shell generated is a "fillOffset" shell. Fill lines are clipped to + * this polygon. Adjusting fillOffset controls bonding of infill to the shells. + * + * Most of this is done in slicePost() in FDM mode. now this is used by SLA, Laser + * + * @param {number} count + * @param {number} offsetN + * @param {number} fillOffset + * @param {Obejct} options + */ +function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { + for (let top of slice.tops) { + doTopShells(slice.z, top, count, offset1, offsetN, fillOffset, opt); + } +} + +/** + * Create an entirely solid layer by filling all top polygons + * with an alternating pattern. + * + * @param {number} linewidth + * @param {number} angle + * @param {number} density + */ + function doSolidLayerFill(slice, spacing, angle) { + if (slice.xray) { + return; + } + + if (slice.tops.length === 0 || typeof(angle) != 'number') { + slice.isSolidLayer = false; + return; + } + + slice.tops.forEach(function(top) { + if (!top.fill_off) return; // missing for inner brick layers + let lines = fillArea(top.fill_off, angle, spacing, null); + top.fill_lines.appendAll(lines); + }); + + slice.isSolidLayer = true; +}; + +/** + * Take output from pluggable sparse infill algorithm and clip to + * the bounds of the top polygons and their inner solid areas. + */ +function doSparseLayerFill(slice, options = {}) { + if (slice.xray || options.type === 'none') { + return; + } + + let process = options.process, + spacing = options.spacing, // spacing space between fill lines + density = options.density, // density of infill 0.0 - 1.0 + bounds = options.bounds, // bounding box of widget + height = options.height, // z layer height + cache = !(options.cache === false), + type = options.type || 'hex'; + + if (slice.tops.length === 0 || density === 0.0 || slice.isSolidLayer || slice.index < 0) { + slice.isSparseFill = false; + return; + } + + let tops = slice.tops, + down = slice.down, + clib = self.ClipperLib, + ctyp = clib.ClipType, + ptyp = clib.PolyType, + cfil = clib.PolyFillType, + clip = new clib.Clipper(), + ctre = new clib.PolyTree(), + poly, + polys = [], + lines = [], + line = [], + solids = [], + // callback passed to pluggable infill algorithm + target = { + // slice and slice property access + slice: function() { return slice }, + zIndex: function() { return slice.index }, + zValue: function() { return slice.z }, + // various option map access + options: function() { return options }, + lineWidth: function() { return options.lineWidth }, + bounds: function() { return bounds }, + zHeight: function() { return height }, + offset: function() { return spacing }, + density: function() { return density }, + repeat: function() { return process.sliceFillRepeat }, + // output functions + emit: function(x,y) { + if (isNaN(x)) { + solids.push(x); + } else { + line.push(newPoint(x, y, slice.z)); + slice.isSparseFill = true; + } + }, + newline: function() { + if (line.length > 0) { + lines.push(line); + line = []; + } + } + }; + + // use specified fill type + if (type && fill[type]) { + fill[type](target); + } else { + console.log({missing_infill: type}); + return; + } + + // force emit of last line + target.newline(); + + // prepare top infill structure + for (let top of tops) { + top.fill_sparse = top.fill_sparse || []; + polys.appendAll(top.fill_off); + polys.appendAll(top.solids); + } + + // update fill fingerprint for this slice + slice._fill_finger = POLY.fingerprint(polys); + + let skippable = cache && fill_fixed[type] ? true : false; + let miss = false; + // if the layer below has the same fingerprint, + // we may be able to clone the infill instead of regenerating it + if (skippable && slice.fingerprintSame(down)) { + // the fill fingerprint can slightly different because of solid projections + if (down._fill_finger && POLY.fingerprintCompare(slice._fill_finger, down._fill_finger)) { + for (let i=0; i { + top.fill_lines.push(p1, p2); + }); + } + } + if (fillable.length) { + let lines = POLY.fillArea(fillable, angl, options.lineWidth); + top.fill_lines.appendAll(lines); + } + } + } + } + + // if only solids were added and no lines to clip + if (!sparse_clip) { + return; + } + + if (options.promises) { + options.promises.push(kiri.minions.clip(slice, polys, lines)); + return; + } + + lines = lines.map(a => a.map(p => p.toClipper())); + clip.AddPaths(lines, ptyp.ptSubject, false); + clip.AddPaths(POLY.toClipper(polys), ptyp.ptClip, true); + + if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { + for (let node of ctre.m_AllPolys) { + poly = POLY.fromClipperNode(node, slice.z); + for (let top of tops) { + // use only polygons inside this top + if (poly.isInside(top.poly)) { + top.fill_sparse.push(poly); + } + } + } + } +}; + +/** + * Find difference between fill inset poly on two adjacent layers. + * Used to calculate bridges, flats and then solid projections. + * 'expand' is used for top offsets in SLA mode + */ +function doDiff(slice, options = {}) const { const { sla, fakedown, grow, min } = options;;if ((slice.index <= 0 && !fakedown) || slice.xray) { + return; + } + const top = slice, + down = slice.down || (fakedown ? newSlice(-1) : null), + topInner = sla ? top.topPolys() : top.topInners(), + downInner = sla ? down.topPolys() : down.topInners(), + bridges = top.bridges = [], + flats = down.flats = []; + + // skip diffing layers that are identical + if (slice.fingerprintSame(down)) { + top.bridges = bridges; + down.flats = flats; + return; + } + + let newBridges = []; + let newFlats = []; + + POLY.subtract(topInner, downInner, newBridges, newFlats, slice.z, min, { + wasm: true + }); + + newBridges = newBridges.filter(p => p.areaDeep() >= min); + newFlats = newFlats.filter(p => p.areaDeep() >= min); + + if (grow > 0 && newBridges.length) { + newBridges = POLY.offset(newBridges, grow); + } + if (grow > 0 && newFlats.length) { + newFlats = POLY.offset(newFlats, grow); + } + + bridges.appendAll(newBridges); + flats.appendAll(newFlats); +}; + +/** + * + * + * @param {Polygon[]} polys + */ +function addSolidFills(slice, polys) { + if (slice.solids) { + slice.solids.appendAll(polys); + } else if (polys && polys.length) { + console.log({no_solids_in: slice, for: polys}) + } +}; + +/** + * project bottom flats down + */ +function projectFlats(slice, count, expand) { + if (!slice.down || !slice.flats) return; + // these flats are marked for finishing print speed + if (slice.flats.length) slice.finishSolids = true; + if (slice && slice.flats && slice.flats.length) { + const flats = expand ? POLY.expand(slice.flats, expand) : slice.flats; + projectSolid(slice, flats, count, false, true); + } +}; + +/** + * project top bridges up + */ +function projectBridges(slice, count) { + if (!slice.up || !slice.bridges) return; + // these flats are marked for finishing print speed + if (slice.bridges.length) slice.finishSolids = true; + projectSolid(slice, slice.bridges, count, true, true); +}; + +/** + * fill projected areas and store line data + * @return {boolean} true if filled, false if not + */ +function doSolidsFill(slice, spacing, angle, minArea, fillQ) { + let minarea = minArea || 1, + tops = slice.tops, + solids = slice.solids; + + if (!(tops && solids)) { + return; + } + + if (slice.isSolidLayer || slice.xray) { + return; + } + + let unioned = POLY.union(solids, undefined, true, { wasm: true }).flat(), + isSLA = (spacing === undefined && angle === undefined); + + if (solids.length === 0) return false; + if (unioned.length === 0) return false; + + let trims = [], + inner = isSLA ? slice.topPolys() : slice.topFillOff(); + + // trim each solid to the inner bounds + for (let p of unioned) { + p.setZ(slice.z); + for (let i of inner) { + let masks = p.mask(i); + if (masks && masks.length > 0) { + trims.appendAll(masks); + } + } + } + + // clear old solids and make array for new + tops.forEach(top => { top.solids = [] }); + + // replace solids with merged and trimmed solids + slice.solids = solids = trims; + + // parent each solid polygon inside the smallest bounding top + let make_solid_layer = false; + let tops_area = tops.length ? tops.map(top => top.poly.areaDeep()).reduce((a,i) => a+i) : 0; + for (let solid of solids) { + for (let top of tops) { + let stop = []; + if (top.poly.overlaps(solid)) { + if (!solid.parent || solid.parent.area() > top.poly.area()) { + if (solid.areaDeep() < minarea) { + // console.log({i:slice.index,cull_solid:solid,area:solid.areaDeep()}); + continue; + } + solid.parent = top.poly; + top.solids.push(solid); + stop.push(solid); + } + } + // problematic for organic shapes with lots of big and small tops + // the small tops tend to trigger entire layer fills. for now just + // trip full solid layer if a single top area diff > 50% + if (false && stop.length) { + let top_area = top.poly.areaDeep(); + let stop_area = stop.map(p => p.areaDeep()).reduce((a,v) => a + v); + // if the solid area > 50% of the top area, make entire layer solid + if (stop_area / tops_area > 0.5) { + make_solid_layer = true; + } + } + } + } + // if 50% of top is filled with solids, trigger layer conversion to solid + // in future, this should be limited to a specific top, not entire layer + if (make_solid_layer) { + for (let top of tops) { + top.solids = []; + } + doSolidLayerFill(slice, spacing, angle); + return; + } + + // for SLA to bypass line infill + if (isSLA) { + return true; + } + + // create empty filled line array for each top + for (let top of tops) { + // synth belt anchor tops don't want fill + if (!top.fill_lines) { + continue; + } + const tofill = []; + const angfill = []; + const newfill = top.fill_lines = []; + // determine fill orientation from top + for (let solid of solids) { + if (solid.parent === top.poly) { + if (solid.fillang) { + angfill.push(solid); + } else { + tofill.push(solid); + } + } + } + if (tofill.length > 0) { + doFillArea(fillQ, tofill, angle, spacing, newfill); + // top.fill_lines_norm = {angle:angle,spacing:spacing}; + } + if (angfill.length > 0) { + top.fill_lines_ang = {spacing:spacing,list:[],poly:[]}; + for (let af of angfill) { + doFillArea(fillQ, [af], af.fillang.angle + 45, spacing, newfill); + // top.fill_lines_ang.list.push(af.fillang.angle + 45); + // top.fill_lines_ang.poly.push(af.clone()); + } + } + } +} + +function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { + if (fillQ) { + fillQ.push(kiri.minions.fill(polys, angle, spacing, output, minLen, maxLen)); + } else { + POLY.fillArea(polys, angle, spacing, output, minLen, maxLen); + } +} + +/** + * calculate external overhangs requiring support + */ +async function doSupport(slice, proc, shadow, opt = {}) { + let maxBridge = proc.sliceSupportSpan || 5, + minArea = proc.supportMinArea || 0.1, + pillarSize = proc.sliceSupportSize, + offset = proc.sliceSupportOffset || 0, + gap = proc.sliceSupportGap, + size = (pillarSize || 1), + tops = slice.topPolys(), + trimTo = tops; + + let traces = POLY.flatten(slice.topShells().clone(true)), + fill = slice.topFill(), + points = [], + down = slice.down, + down_tops = down ? down.topPolys() : null, + down_traces = down ? POLY.flatten(down.topShells().clone(true)) : null; + + if (opt.exp && down_tops) { + let points = down_tops.map(p => p.deepLength).reduce((a,v)=>a+v); + if (points > 200) { + // use de-rez'd top shadow instead + down_tops = down.topSimples(); + // de-rez trace polys because it's not that important for supports + down_traces = down_traces.map(p => p.clean(true, undefined, config.clipper / 10)); + } + } + + // DEBUG code + let SDBG = false; + let cks = SDBG ? [] : undefined; + let pip = SDBG ? [] : undefined; + let pcl = SDBG ? [] : undefined; + + // check if point is supported by layer below + function checkPointSupport(point) { + if (SDBG) cks.push(point); // DEBUG + // skip points close to other support points + for (let i=0; i= maxBridge) { + let slope = p1.slopeTo(p2).factor(1/dist), + segs = Math.floor(dist / maxBridge) + 1, + seglen = dist / segs; + while (i < segs) { + checkPointSupport(p1.projectOnSlope(slope, i++ * seglen)); + } + } + if (poly) checkPointSupport(p2); + } + + let supports = []; + + // generate support polys from unsupported points + if (slice.down) (function() { + // check trace line support needs + traces.forEach(function(trace) { + trace.forEachSegment(function(p1, p2) { checkLineSupport(p1, p2, true) }); + }); + + // add offset solids to supports (or fill depending) + fill.forEachPair(function(p1,p2) { checkLineSupport(p1, p2, false) }); + + // skip the rest if no points or supports + if (!(points.length || supports.length)) return; + + let pillars = []; + + // for each point, create a bounding rectangle + points.forEach(function(point) { + pillars.push(base.newPolygon().centerRectangle(point, size/2, size/2)); + }); + + supports.appendAll(POLY.union(pillars, null, true, { wasm: false })); + // merge pillars and replace with convex hull of outer points (aka smoothing) + pillars = POLY.union(pillars, null, true, { wasm: false }).forEach(function(pillar) { + supports.push(base.newPolygon().createConvexHull(pillar.points)); + }); + })(); + + // DEBUG code + if (SDBG && down_traces) slice.output() + .setLayer('cks', { line: 0xee5533, check: 0xee5533 }) + .addPolys(cks.map(p => base.newPolygon().centerRectangle(p, 0.25, 0.25))) + .setLayer('pip', { line: 0xdd4422, check: 0xdd4422 }) + .addPolys(pip.map(p => base.newPolygon().centerRectangle(p, 0.4, 0.4))) + .setLayer('pcl', { line: 0xcc3311, check: 0xcc3311 }) + .addPolys(pcl.map(p => base.newPolygon().centerRectangle(p, 0.3, 0.3))) + .setLayer('pts', { line: 0xdd33dd, check: 0xdd33dd }) + .addPolys(points.map(p => base.newPolygon().centerRectangle(p, 0.8, 0.8))) + .setLayer('dtr', { line: 0x0, check: 0x0 }) + .addPolys(POLY.setZ(down_traces.clone(true),slice.z)); + ; + + if (supports.length === 0) { + return; + } + + // then union supports + if (supports.length > 10) { + supports = await kiri.minions.union(supports); + } else { + supports = POLY.union(supports, null, true, { wasm: false }); + } + + // clip to top polys + supports = POLY.trimTo(supports, shadow); + + let depth = 0; + while (down && supports.length > 0) { + down.supports = down.supports || []; + + let trimmed = [], culled = []; + + // culled = supports; + // clip supports to shell offsets + POLY.subtract(supports, down.topSimples(), trimmed, null, slice.z, minArea, { wasm: false }); + + // set depth hint on support polys for infill density + trimmed.forEach(function(trim) { + if (trim.area() < minArea) return; + culled.push(trim.setZ(down.z)); + }); + + // exit when no more support polys exist + if (culled.length === 0) break; + + // new bridge polys for next pass (skip first layer below) + if (depth >= gap) { + down.supports.appendAll(culled); + } + + supports = culled; + down = down.down; + depth++; + } + +} + +function doSupportFill(args) const { const { promises, slice, lineWidth, density, minArea, isBelt, angle, outline } = args;;let supports = slice.supports, + nsB = [], + nsC = [], + min = minArea || 0.1; + + if (!supports) return; + + // union supports + supports = POLY.setZ(POLY.union(supports, undefined, true, { wasm: false }), slice.z); + + // clip supports to slice clip offset (or shell if none) + POLY.subtract(supports, slice.clips, nsB, null, slice.z, min, { wasm: false }); + supports = nsB; + + // also trim to lower offsets, if they exist + if (slice.down && slice.down.clips) { + POLY.subtract(nsB, slice.down.clips, nsC, null, slice.z, min, { wasm: false }); + supports = nsC; + } + + if (supports) { + fillSupportPolys({ + promises, polys: supports, lineWidth, density, z: slice.z, isBelt, angle, outline + }); + } + + // re-assign new supports back to slice + slice.supportOutline = supports; + slice.supports = supports; +}; + +function fillSupportPolys(args) const { const { promises, polys, lineWidth, density, z, isBelt, angle, outline } = args;;// calculate fill density + let spacing = lineWidth * (1 / density); + polys.forEach(function (poly) { + // calculate angle based on width/height ratio + let auto = isBelt || (poly.bounds.width() / poly.bounds.height() > 1) ? 1090 : 1000; + // inset support poly for fill lines 33% of nozzle width + let inset = POLY.offset([poly], -lineWidth/3, {flat: true, z, wasm: true}); + // do the fill + if (inset && inset.length > 0) { + doFillArea(promises, inset, angle || auto, spacing, poly.fill = []); + if (!outline && poly.fill.length) { + poly.fill = connect_lines(poly.fill, spacing * 2); + } + } + return true; + }); +} + +/** + * + * @param {Slice} slice + * @param {Polygon[]} polys + * @param {number} count + * @param {boolean} up + * @param {boolean} first + * @returns {*} + */ +function projectSolid(slice, polys, count, up, first) { + if (!slice || count <= 0) { + return; + } + let clones = polys.clone(true); + if (first) { + clones.forEach(function(p) { + p.hintFillAngle(); + }); + } + addSolidFills(slice, clones); + if (count > 0) { + if (up) projectSolid(slice.up, polys, count-1, true, false); + else projectSolid(slice.down, polys, count-1, false, false); + } +} + +FDM.supports = function(settings, widget) { + let isBelt = settings.device.bedBelt; + let process = settings.process; + let size = process.sliceSupportSize; + let s9 = size / 9; + let s4 = size / 4; + let s2 = size * 0.45; + let min = 0.01; + let geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(widget.vertices, 3)); + let rad = (Math.PI / 180); + let deg = (180 / Math.PI); + let angle = rad * settings.process.sliceSupportAngle; + let thresh = -Math.sin(angle); + let dir = new THREE.Vector3(0,0,-1) + let add = []; + let mat = new THREE.MeshBasicMaterial(); + let mesh = new THREE.Mesh(geo, mat); + let platform = new THREE.Mesh( + new THREE.PlaneGeometry(10000,10000,1), mat + ); + const now = Date.now(); + // test point + function tp(point) { + if (point.added) { + return; + } + // omit pillars close to existing pillars + for (let added of add) { + let p2 = new THREE.Vector2(point.x, point.y); + let pm = new THREE.Vector2(added.mid.x, added.mid.y); + if (Math.abs(point.z - added.from.z) < s2 && p2.distanceTo(pm) < s4) { + return; + } + } + let ray = new THREE.Raycaster(point, dir); + let int = ray.intersectObjects([ mesh, platform ], false); + if (int && int.length && int[0].distance > 0.5) { + let mid = new THREE.Vector3().add(point).add(int[0].point).divideScalar(2); + add.push({from: point, to: int[0].point, mid}); + point.added = true; + } + } + function tf(a, b, c) { + let dab = a.distanceTo(b); + let dbc = b.distanceTo(c); + let dca = c.distanceTo(a); + let max = Math.max(dab, dbc, dca); + if (max < size) { + let min = Math.min(dab, dbc, dca); + if (min < s9 && Math.random() < 0.5) { + return; + } + // test midpoint of tri face + return tp(new THREE.Vector3().add(a).add(b).add(c).divideScalar(3)); + } + if (dab === max) { + let mp = new THREE.Vector3().add(a).add(b).divideScalar(2); + tf(mp, b, c); + tf(a, mp, c); + } else if (dbc === max) { + let mp = new THREE.Vector3().add(b).add(c).divideScalar(2); + tf(a, mp, c); + tf(a, b, mp); + } else { + let mp = new THREE.Vector3().add(c).add(a).divideScalar(2); + tf(a, b, mp); + tf(mp, b, c); + } + } + + let filter = isBelt ? (norm) => { + return norm.z <= thresh && norm.y < 0; + } : (norm) => { + return norm.z < thresh; + }; + let const { position } = geo.attributes;;let const { itemSize, count, array } = position;;for (let i = 0; i 0; +}; + +class Vector3Cache { + constructor() { + this.cache = {}; + } + + get(x, y, z) { + let key = [x.round(4),y.round(4),z.round(4)].join(','); + let val = this.cache[key]; + if (!val) { + val = new THREE.Vector3(x, y, z); + this.cache[key] = val; + } + return val; + } +} + +class Coplanars { + constructor() { + this.cache = {}; + } + + put(a, b, c, norm) { + let key = norm.round(7).toString(); + let arr = this.cache[key]; + if (!arr) { + arr = []; + this.cache[key] = arr; + } + arr.push([a,b,c]); + } + + group(union) { + let out = {}; + for (let norm in this.cache) { + let arr = this.cache[norm]; + let groups = []; + for (let face of arr) { + let match = undefined; + // see if face matches vertices in any group + outer: for (let group of groups) { + for (let el of group) { + if ( + el.indexOf(face[0]) >= 0 || + el.indexOf(face[1]) >= 0 || + el.indexOf(face[2]) >= 0 + ) { + match = group; + break outer; + } + } + } + if (match) { + match.push(face); + } else { + groups.push([face]); + } + } + if (union) { + // convert groups of faces to contiguous polygon groups + groups = groups.map(group => { + let parr = group.map(arr => { + return base.newPolygon() + .add(arr[0].x, arr[0].y, arr[0].z) + .add(arr[1].x, arr[1].y, arr[1].z) + .add(arr[2].x, arr[2].y, arr[2].z); + }); + let union = parr.length === 1 ? parr : + POLY.union(parr, 0, true, {wasm:false}); + union.merged = parr.length; + union.face = group[0]; + return union; + }); + } + out[norm] = groups; + } + // console.log(out); + return out; + } +} + + +export { vopt, doShadow, onSliceDone, doupdate, trackupdate, forSlices, connect_lines, bound, doRender, doShells, doSolidLayerFill, doSparseLayerFill, doDiff, addSolidFills, projectFlats, projectBridges, doSolidsFill, doFillArea, doSupport, checkPointSupport, checkLineSupport, doSupportFill, fillSupportPolys, projectSolid, tp, tf, POLY, isThin, widgets, tl, ext, e, render, color, proto, sliceMinHeight, bounds, points, indices, heights, healed, zPress, zCut, zd, h1, h0, hm, h, z, zi, zh, zIncFirst, zInc, zIncMin, zHeights, zIndexes, zOrdered, zPos, realz, slice, alltops, shadow, found, i, gap, tops, clipto, ntops, range, spaceMult, isBottom, isTop, isDense, isSolid, solidWidth, fillSpace, smin, miny, y, by, minx, peek, brim, adds, step, anchorlen, addto, splat, snew, bump, count, poly, first, mp, dx, fy, fz, n2, x, index, p0, p1, p0y, p1y, i_ok, y_ok, x_ok, dy, dz, span, tall, can, ctx, img, rgb, str, maxp, lines, pix, supps, params, solidMinArea, sliceFillGrow, promises, newType, down, outline, resolve, density, supports, polys, last, nu, nuSlice, ti, nuTop, cap, newlines, eo, p2, t, op1, idx, Color, output, height, group, process, skippable, miss, sparse_clip, masks, angl, inter, p, top, newBridges, newFlats, flats, minarea, unioned, trims, make_solid_layer, tops_area, stop, top_area, stop_area, tofill, angfill, newfill, maxBridge, traces, SDBG, cks, pip, pcl, supported, dist, slope, pillars, depth, trimmed, spacing, auto, inset, clones, isBelt, size, s9, s4, s2, min, geo, rad, deg, angle, thresh, dir, add, mat, mesh, platform, now, pm, ray, int, mid, dab, dbc, dca, max, filter, ip, a, b, c, norm, key, arr, out, groups, match, parr, union }; \ No newline at end of file diff --git a/src2/kiri-mode/laser/driver.js b/src2/kiri-mode/laser/driver.js new file mode 100644 index 00000000..277c0f52 --- /dev/null +++ b/src2/kiri-mode/laser/driver.js @@ -0,0 +1,857 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../kiri/api.js'; +import { Packer } from '../../kiri/pack.js'; +import { newPoint } from '../../geo/point.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { render } from '../../kiri/render.js'; +import { Slice, newSlice } from '../../kiri/slice.js'; + +const TYPE = { + LASER: 0, + DRAG: 1, + WJET: 2, + WEDM: 3 +}; + +export const driver = { + TYPE, + type: TYPE.LASER, + name: 'Laser', + init, + slice, + prepare, + export: exportLaser, + exportGCode, + exportSVG, + exportDXF, +}; + +function init() { + api.event.on("settings.saved", settings => { + // TODO + }); +} + +function polyLabel(poly, label) { + console.log('label', label); +} + +function sliceEmitObjects(print, slice, groups, opt = { }) { + let process = print.settings.process, + { ctOutMark, ctOutStack, ctOutGroup, ctOutShaper, outputLaserLabel } = process, + grouped = ctOutMark || ctOutGroup, + label = false && outputLaserLabel, + simple = opt.simple || false, + emit = { in: [], out: [], mark: [] }, + lastEmit = opt.lastEmit, + zcolor = print.settings.process.ctOutZColor; + + function polyOut(poly, group, type, indexed) { + if (!poly) { + return; + } + if (Array.isArray(poly)) { + poly.forEach((pi, index) => { + polyOut(pi, group, type, indexed ? index : undefined); + }); + } else { + let pathOpt = zcolor ? {extrude: slice.z, rate: slice.z} : {extrude: 1, rate: 1}; + if (type === "mark") { + pathOpt.rate = 0.001; + pathOpt.extrude = 2; + } else if (label && type === "out") { + let lbl = indexed !== undefined ? `${slice.index}-${indexed}` : `${slice.index}`; + polyLabel(poly, lbl); + } + if (simple) { + pathOpt.simple = true; + } + if (poly.open) { + pathOpt.open = true; + } + emit[type].push(poly); + print.PPP(poly, group, pathOpt); + // when stacking (top down widget slices), if the last layer is fully contained + // by the current layer, then it is "marked" onto the current layer in a different color + if (ctOutMark && type === "out" && lastEmit) { + for (let out of lastEmit.out) { + if (out.isInside(poly)) { + polyOut(out.clone().setZ(poly.getZ()), group, "mark"); + } + } + } + } + } + + if (grouped) { + let group = []; + let outer = slice.offset; + let inner = outer.map(poly => poly.inner || []).flat(); + // preserve slice thickness in group + group.thick = slice.thick; + // cut inside before outside + polyOut(inner, group, "in", outer.length > 1); + polyOut(outer, group, "out", outer.length > 1); + groups.push(group); + } else { + for (let top of slice.offset) { + let group = []; + group.thick = slice.thick; + polyOut([ top ], group, "in"); + polyOut(top.inner || [], group, "out"); + groups.push(group); + } + } + + return emit; +}; + +// convert arc into line segments +function arc(center, rad, s1, s2, out) { + let a1 = s1.angle; + let step = 5; + let diff = s1.angleDiff(s2, true); + let ticks = Math.abs(Math.floor(diff / step)); + let dir = Math.sign(diff); + let off = (diff % step) / 2; + if (off == 0) { + ticks++; + } else { + out.push( center.projectOnSlope(s1, rad) ); + } + while (ticks-- > 0) { + out.push( center.projectOnSlope(base.newSlopeFromAngle(a1 + off), rad) ); + a1 += step * dir; + } + out.push( center.projectOnSlope(s2, rad) ); +} + +// start to the "left" of the first point +function addKnifeRadii(poly, tipoff) { + poly.setClockwise(); + let oldpts = poly.points.slice(); + // find leftpoint and make that the first point + let start = oldpts[0]; + let startI = 0; + for (let i=1; i start.y) { + start = pt; + startI = i; + } + } + if (startI > 0) { + oldpts = oldpts.slice(startI,oldpts.length).appendAll(oldpts.slice(0,startI)); + } + + let lastpt = oldpts[0].clone().move({x:-tipoff,y:0,z:0}); + let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); + let newpts = [ lastpt, lastpt = oldpts[0].clone() ]; + let tmp; + for (let i=1; i= 10) { + if (lastpt.distTo2D(nextpt) >= tipoff) { + arc(lastpt, tipoff, lastsl, nextsl, newpts); + } else { + // todo handle short segments + // newpts.push(lastpt.projectOnSlope(lastsl, tipoff) ); + // newpts.push( lastpt.projectOnSlope(nextsl, tipoff) ); + } + } + newpts.push(nextpt); + lastsl = nextsl; + lastpt = nextpt; + } + newpts.push( tmp = lastpt.projectOnSlope(lastsl, tipoff) ); + newpts.push( tmp.clone().move({x:tipoff, y:0, z: 0}) ); + poly.open = true; + poly.points = newpts; +} + +/** + * DRIVER SLICE CONTRACT + * + * @param {Object} settings + * @param {Widget} Widget + * @param {Function} onupdate (called with % complete and optional message) + * @param {Function} ondone (called when complete with an array of Slice objects) + */ +function slice(settings, widget, onupdate, ondone) { + let proc = settings.process; + let offset = proc.ctSliceKerf || 0; + let color = settings.controller.dark ? 0xbbbbbb : 0; + + if (proc.ctSliceHeight < 0) { + return ondone("invalid slice height"); + } + + let { ctSliceSingle, ctSliceHeight, ctSliceHeightMin, ctOmitInner } = proc; + let bounds = widget.getBoundingBox(); + let points = widget.getPoints(); + let indices = []; + + base.slice(points, { + zMin: bounds.min.z, + zMax: bounds.max.z, + zGen(zopt) { + let { zMin, zMax, zIndexes } = zopt; + if (ctSliceSingle) { + indices = [ ctSliceHeight ]; + } else if (ctSliceHeight) { + for (let z = zMin + ctSliceHeight / 2; z < zMax; z += ctSliceHeight) { + indices.push(z); + } + indices; + } else { + for (let i = 1; i < zIndexes.length; i++) { + indices.push((zIndexes[i-1] + zIndexes[i]) / 2); + } + // discard layers too + if (ctSliceHeightMin) { + let last; + indices = indices.filter(v => { + let ok = true; + if (last !== undefined && Math.abs(v - last) < ctSliceHeightMin) { + ok = false; + } else { + last = v; + } + return ok; + }); + } + } + return indices; + }, + onupdate(v) { + onupdate(v); + } + }).then(output => { + // ensure slice order is high Z to low Z for stacking + widget.slices = output.slices.map(data => { + let { z, lines, groups } = data; + let tops = POLY.nest(groups); + // for WireEDM, we commonly omit inner polys because we can't + // navigate to cut them separately unlike laser, water, drag + if (ctOmitInner) tops.forEach(top => delete top.inner); + let slice = newSlice(z).addTops(tops); + slice.index = indices.indexOf(z); + // create top offsets (even if 0) + let offsets = slice.offset = offset ? + POLY.offset(tops, offset, {z: slice.z, miter: 2 / offset}) : tops; + slice.output().setLayer("object", { line: 0x888800 }).addPolys(tops); + slice.output().setLayer("cut", { line: color }).addPolys(offsets); + return slice; + }).sort((a,b) => b.z - a.z); + ondone(); + }); +}; + +/** + * DRIVER PRINT CONTRACT + * + * @param {Object} print state object + * @param {Function} update incremental callback + */ +async function prepare(widgets, settings, update) { + let device = settings.device, + process = settings.process, + print = self.worker.print = newPrint(settings, widgets), + isWire = self.worker.mode === 'WEDM', + isKnife = self.worker.mode === 'DRAG', + isLaser = self.worker.mode === 'LASER', + output = print.output = [], + totalSlices = 0, + slices = 0, + maxZ = 0, + { bedWidth, bedDepth } = device, + { ctOutLayer, ctOutTileSpacing } = process, + { ctOriginCenter, ctOriginBounds } = process, + { ctOriginOffX, ctOriginOffY } = process, + { ctOutStack, ctOutMerged, ctOutKnifeTip, ctOutShaper } = process; + + ctOutStack = ctOutStack && isLaser; + + // shim to adapt older code -- should be refactored + // let pppo = []; + let pppg = []; + let tile = []; + let tiles = []; + print.PPP = function(poly, group, options) { + if (!pppg.contains(group)){ + pppg.push(group); + tiles.push(tile = []); + tile.num = tiles.length; + } + // pppo.push({ poly, group, options }); + tile.push({ poly, group, options }); + }; + + // filter ignored widgets + widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); + + // find max layers (for updates) and Z (for shaper) + for (let widget of widgets) { + totalSlices += widget.slices.length; + maxZ = Math.max(maxZ, widget.bounds.max.z); + } + + // emit objects from each slice into output array + let layers = []; + for (let widget of widgets) { + if (ctOutStack) { + // 3d stack output, no merging or layout + for (let slice of widget.slices) { + let group = []; + let polys = []; + for (let poly of slice.offset) { + if (poly.inner) { + polys.push(...poly.inner); + } + polys.push(poly); + } + for (let poly of polys) { + print.PPP(poly, group, { + extrude: 1, + rate: 1, + }); + } + } + } else if (ctOutMerged) { + // slice stack merging, no layout + // there are no inner vs outer polys + // merged polys "increase" their color / weight + let merged = []; + for (let slice of widget.slices) { + let polys = []; + slice.offset.clone(true).forEach(p => p.flattenTo(polys)); + for (let poly of polys) { + let match = false; + for (let i=0; i { + addKnifeRadii(ip, ctOutKnifeTip); + }); + } + } + } + let lastEmit; + let slices = ctOutStack ? widget.slices.reverse() : widget.slices; + for (let slice of slices) { + lastEmit = sliceEmitObjects(print, slice, layers, {simple: isKnife, lastEmit}); + update((slices++ / totalSlices) * 0.5, "prepare"); + } + } + } + + // for tile layout packing + let dw = device.bedWidth / 2, + dh = device.bedDepth / 2, + sort = !ctOutLayer, + spacing = ctOutTileSpacing; + + // compute tile bounds + for (let tile of tiles) { + let bounds = base.newBounds(); + tile.forEach(rec => bounds.merge(rec.poly.bounds)); + tile.w = bounds.width(); + tile.h = bounds.height(); + tile.bounds = bounds; + } + + // pack tiles + let tp = new Packer(dw, dh, spacing, { invy:isWire, simple: !sort }).pack(tiles, packer => { + return packer.rescale(1.1, 1.1); + }); + + // reposition tiles into their packed locations (unless 3d stack) + if (!ctOutStack) + for (let tile of tiles) { + let { fit, bounds } = tile; + for (let { poly } of tile) { + poly.setZ(0); + poly.move({ + x: fit.x - (tp.max.w / 2) - bounds.minx, + y: fit.y - (tp.max.h / 2) - bounds.miny, + z: 0}, true); + } + tile.bounds = base.newBounds(); + for (let { poly } of tile) { + tile.bounds.merge(poly.bounds); + } + } + + // set starting point based on origin type + let currentPos = + ctOriginCenter ? newPoint(0,0,0) : + ctOriginBounds ? newPoint( + - tp.max.w/2 - (ctOriginOffX || 0), + - tp.max.h/2 - (ctOriginOffY || 0), 0 + ) : + newPoint(-dw, -dh, 0); + + // output tiles starting with closest to origin + let remain = tiles.slice(); + while (remain.length) { + let closest = ctOutStack ? + { dist: 0, tile: remain[0] } : + { dist: Infinity }; + for (let tile of remain) { + for (let { poly } of tile) { + for (let p of poly.points) { + let pd = currentPos.distTo2D(p); + if (pd < closest.dist) { + closest.dist = pd; + closest.tile = tile; + } + } + } + } + if (!closest.tile) throw "no closest found"; + for (let { poly, group, options } of closest.tile) { + currentPos = print.polyPrintPath(poly, currentPos, group, options); + if (!output.contains(group)) { + output.push(group); + } + } + remain.remove(closest.tile); + } + + // clone output points to prevent double offsetting during export + for (let layer of output) { + for (let rec of layer) { + rec.point = rec.point.clone(); + } + } + + return render.path(output, (progress, layer) => { + if (ctOutShaper) { + for (let layer of output) { + for (let out of layer) { + out.point.zi = maxZ - out.point.z; + } + } + } + update(0.5 + progress * 0.5, "render", layer); + }, { thin: true, z: ctOutStack ? undefined : 0, action: "cut", moves: process.isKnife }); +}; + +function exportLaser(print, online, ondone) { + ondone(print.output); +} + +/** + * + */ +function exportElements(settings, output, onpre, onpoly, onpost, onpoint, onlayer) { + let { process, device } = settings, + { bedWidth, bedDepth } = device, + { ctOriginCenter, ctOriginBounds } = process, + { outputInvertX, outputInvertY } = process, + { ctOriginOffX, ctOriginOffY } = process, + { ctOutPower, ctOutSpeed, maxZ } = process, + last, + point, + poly = [], + off = ctOriginBounds ? {x:ctOriginOffX||0, y:ctOriginOffY||0} : {x:0, y:0}, + min = {x:0, y:0}, + max = {x:0, y:0}, + size = {w:0, h:0}; + + // compute bounds for entire output + output.forEach(layer => { + layer.forEach(out => { + point = out.point; + if (outputInvertX) point.x = -point.x; + if (outputInvertY) point.y = -point.y; + min.x = Math.min(min.x, point.x); + max.x = Math.max(max.x, point.x); + min.y = Math.min(min.y, point.y); + max.y = Math.max(max.y, point.y); + }); + }); + size.w = max.x - min.x; + size.h = max.y - min.y; + + let bounds = base.newBounds(); + if (ctOriginCenter) { + // place origin at geometric center of all points + // regardless of workspace size + for (let layer of output) { + for (let out of layer) { + point = out.point; + point.x = point.x - min.x - size.w / 2 + off.x; + point.y = point.y - min.y - size.h / 2 + off.y; + bounds.update(point); + } + } + max.x = max.x - min.x + off.x; + max.y = max.y - min.y + off.y; + min.x = 0; + min.y = 0; + } else if (ctOriginBounds) { + // place origin at min x,y of all points + for (let layer of output) { + for (let out of layer) { + point = out.point; + point.x = point.x - min.x + off.x; + point.y = point.y - min.y + off.y; + bounds.update(point); + } + } + max.x = max.x - min.x + off.x; + max.y = max.y - min.y + off.y; + min.x = 0; + min.y = 0; + } else { + // place origin at min x,y taking into account + // each layer's relative position + let w = bedWidth; + let h = bedDepth; + for (let layer of output) { + for (let out of layer) { + point = out.point; + point.x += w / 2 + off.x; + point.y += h / 2 + off.y; + bounds.update(point); + } + } + max.x = w + off.x; + max.y = h + off.y; + min.x = 0; + min.y = 0; + } + + onpre(min, max, ctOutPower, ctOutSpeed); + + // output each layer with moves between them + output.forEach((layer, index) => { + let thick = 0; + let color = 0; + layer.forEach((out, li) => { + thick = out.thick; + point = out.point; + if (onlayer) { + onlayer(index, point.z, out.thick, output.length); + } + if (out.emit) { + color = out.emit; + if (last && poly.length === 0) { + poly.push(onpoint(last)); + } + poly.push(onpoint(point)); + } else if (poly.length > 0) { + onpoly(poly, color, thick); + poly = []; + } + last = point; + }); + if (poly.length > 0) { + onpoly(poly, color, thick); + poly = []; + } + }); + + onpost(); +}; + +/** + * + */ +function exportGCode(settings, data) { + let lines = [], dx = 0, dy = 0, z = 0, feedrate; + let dev = settings.device; + let proc = settings.process; + let space = dev.gcodeSpace ? ' ' : ''; + let max_power = dev.laserMaxPower || 255; + let power = max_power; + let cut_on = dev.gcodeLaserOn || dev.gcodeWaterOn || dev.gcodeKnifeDn || []; + let cut_off = dev.gcodeLaserOff || dev.gcodeWaterOff || dev.gcodeKnifeUp || []; + let isKnife = proc.isKnife; + let knifeDepth = proc.ctOutKnifeDepth; + let passes = isKnife ? proc.ctOutKnifePasses : 1; + + exportElements( + settings, + data, + function(min, max, pct, speed) { + let width = (max.x - min.x), + height = (max.y - min.y); + + dx = min.x; + dy = min.y; + feedrate = `${space}F${speed}`; + power = (max_power * (pct / 100)).toFixed(3); + + (dev.gcodePre || []).forEach(line => { + lines.push(line); + }); + }, + function(poly, color, thick) { + if (isKnife) { + lines.push(`; start new poly id=${poly.id} len=${poly.length}`); + } + for (let i=1; i { + line = line.replace('{power}', power); + line = line.replace('{color}', color); + line = line.replace('{thick}', thick); + line = line.replace('{z}', z); + lines.push(line); + }); + lines.push(`G1${space}${point}${feedrate}`); + } else { + lines.push(`G1${space}${point}`); + } + }); + cut_off.forEach(line => { + lines.push(line); + }); + } + if (isKnife) { + lines.appendAll([`G0${space}Z5`]); + } + }, + function() { + (dev.gcodePost || []).forEach(line => { + lines.push(line); + }); + }, + function(point) { + z = point.z; + return `X${(point.x - dx).toFixed(3)}${space}Y${(point.y - dy).toFixed(3)}`; + }, + function(layer, z, thick, layers) { + // ununsed + } + ); + + return lines.join('\n'); +}; + +/** + * + */ +function exportSVG(settings, data, cut_color) { + let { process } = settings; + let { ctOutInches, ctOutStack, ctOutShaper } = process; + let swidth = "0.1mm"; + let zcolor = process.ctOutZColor ? 1 : 0; + let lines = [], dx = 0, dy = 0, my, z = 0; + let colors = [ + "black", + "purple", + "blue", + "red", + "orange", + "yellow", + "green", + "brown", + "gray" + ]; + + exportElements( + settings, + data, + function(min, max) { + let width = (max.x - min.x), + height = (max.y - min.y); + dx = min.x; + dy = min.y; + my = max.y; + lines.push(''); + lines.push(''); + let svg = [''); + }, + function(poly, color, thick) { + let cout = zcolor || colors[((color-1) % colors.length)]; + if (ctOutStack) { + let def = ["path"]; + if (z !== undefined && ctOutShaper) { + if (ctOutInches) { + def.push(`shaper:cutDepth="${(z / 25.4).toFixed(2)}in"`); + } else { + def.push(`shaper:cutDepth="${(z).toFixed(2)}mm"`); + } + def.push(`shaper:cutType="online"`); + // def.push(`shaper:cutOffset="0in"`); + // def.push(`shaper:toolDia="0in"`); + } else if (z !== undefined) { + def.push(`z="${z}"`); + } + let path = poly.map((xy, i) => i > 0 ? `L${xy}` : `M${xy}`); + let std = ctOutShaper ? '' : `fill="none" stroke="${cout}" stroke-width="${swidth}" `; + lines.push(`<${def.join(' ')} d="${path.join(' ')}" ${std}/>`); + } else { + let def = ["polyline"]; + if (z !== undefined) def.push(`z="${z}"`); + if (thick !== undefined) def.push(`h="${thick}"`); + lines.push(`<${def.join(' ')} points="${poly.join(' ')}" fill="none" stroke="${cout}" stroke-width="${swidth}" />`); + } + }, + function() { + lines.push(""); + }, + function(point) { + z = ctOutStack ? (point.zi ?? point.z) : undefined; + let px = point.x - dx; + let py = my - point.y - dy; + // convert to imperial for stacked + if (ctOutInches) { + px /= 25.4; + py /= 25.4; + } + return base.util.round(px, 3) + "," + base.util.round(py, 3); + }, + function(layer, z, thick, layers) { + if (zcolor) { + zcolor = Math.round(((layer + 1) / layers) * 0xffffff).toString(16); + zcolor = `#${zcolor.padStart(6,0)}`; + } + } + ); + + return lines.join('\n'); +}; + +/** + * + */ +function exportDXF(settings, data) { + let lines = []; + + exportElements( + settings, + data, + function(min, max) { + lines.appendAll([ + " 0", + "SECTION", + " 2", + "HEADER", + " 0", + "ENDSEC", + " 0", + "SECTION", + " 2", + "TABLES", + " 0", + "ENDSEC", + " 0", + "SECTION", + " 2", + "BLOCKS", + " 0", + "ENDSEC", + " 0", + "SECTION", + " 2", + "ENTITIES", + ]); + }, + function(poly) { + lines.appendAll([ + " 0", + "POLYLINE", + " 8", + "0", + " 66", + "1", + " 70", + "1", + ]); + + poly.forEach(point => { + lines.appendAll([ + ' 0', + 'VERTEX', + ' 8', + '0', + ' 10', + point.x, + ' 20', + point.y + ]); + }); + + lines.appendAll([ + ' 0', + 'SEQEND', + ]); + }, + function() { + lines.appendAll([ + ' 0', + 'ENDSEC', + ' 0', + 'SECTION', + ' 2', + 'OBJECTS', + ' 0', + 'ENDSEC', + ' 0', + 'EOF', + ]); + }, + function(point) { + return {x:point.x,y:point.y}; + } + ); + + return lines.join('\n'); +}; diff --git a/src2/kiri-mode/sla/client.js b/src2/kiri-mode/sla/client.js new file mode 100644 index 00000000..d35a6d81 --- /dev/null +++ b/src2/kiri-mode/sla/client.js @@ -0,0 +1,88 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +export const client = { + init, + printDownload +}; + +function init(kiri, api) { + api.event.on("mode.set", (mode) => { + if (mode === 'SLA') { + api.ui.func.preview.classList.add('hide'); + } else { + api.ui.func.preview.classList.remove('hide'); + } + }); +} + + function printDownload(output, api, names) { + const { file, width, height, layers, volume } = output; + const fileroot = names[0] || "print"; + const filename = `${fileroot}-${new Date().getTime().toString(36)}`; + + api.modal.show('xsla'); + // api.ajax("/kiri/output-sla.html", html => { + // api.ui.print.innerHTML = html; + + let settings = api.conf.get(), + process = settings.process, + device = settings.device, + print_sec = (process.slaBaseLayers * process.slaBaseOn) + + (layers - process.slaBaseLayers) * process.slaLayerOn; + + // add peel lift/drop times to total print time + for (let i=0; i { saveFile(api, file, ".photon") }; + break; + case 'Anycubic.Photon.S': + download.innerText += " .photons"; + download.onclick = () => { saveFile(api, file, ".photons") }; + break; + case 'Creality.Halot.Sky': + default: + download.innerText += " .cxdlp"; + download.onclick = () => { saveFile(api, file, ".cxdlp") }; + break; + } + + // api.modal.show('print'); + // }); +} + +function saveFile(api, file, ext) { + api.util.download(file, $('print-filename-sla').value + ext); + api.modal.hide(); +} diff --git a/src2/kiri-mode/sla/driver.js b/src2/kiri-mode/sla/driver.js new file mode 100644 index 00000000..dfdf00f1 --- /dev/null +++ b/src2/kiri-mode/sla/driver.js @@ -0,0 +1,42 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { newPrint } from '../../kiri/print.js'; + +export const driver = { + prepare, + legacy: false +}; + +if (driver.legacy) { + console.log("SLA Driver in Legacy Mode"); +} + +// runs in worker. would usually be in src/mode/sla/prepare.js +// but the SLA driver skips the prepare step because there is no path routing +async function prepare(widgets, settings, update) { + self.worker.print = newPrint(settings, widgets); + if (!driver.wasm) { + fetch('/wasm/kiri-sla.wasm') + .then(response => response.arrayBuffer()) + .then(bytes => WebAssembly.instantiate(bytes, { + env: { + reportf: (a,b) => { console.log('[f]',a,b) }, + reporti: (a,b) => { console.log('[i]',a,b) } + } + })) + .then(results => { + let {module, instance} = results; + let {exports} = instance; + let heap = new Uint8Array(exports.memory.buffer); + driver.wasm = { + heap, + memory: exports.memory, + render: exports.render, + rle_encode: exports.rle_encode + }; + }); + } + update(1); +} + +export { prepare }; diff --git a/src2/kiri-mode/sla/export.js b/src2/kiri-mode/sla/export.js new file mode 100644 index 00000000..7a0887f3 --- /dev/null +++ b/src2/kiri-mode/sla/export.js @@ -0,0 +1,172 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { base } from '../geo/base.js'; +import { driver } from '../kiri-mode/sla/driver.js'; +import { x_cxdlp } from '../kiri-mode/sla/x_cxdlp.js'; +import { x_photon } from '../kiri-mode/sla/x_photon.js'; + + +const { base, kiri } = root +const { driver } = kiri; +const { util } = base; +const { SLA } = driver; + +/** + * DRIVER CONTRACT - runs in worker + * @param {Object} print state object + * @param {Function} online streaming reply + * @param {Function} ondone last reply + */ +SLA.export = function(print, online, ondone) { + let widgets = print.widgets, + settings = print.settings, + device = settings.device, + process = settings.process, + width = device.resolutionX, + height = device.resolutionY, + scaleX = width / device.bedWidth, + scaleY = height / device.bedDepth, + layerZ = process.slaSlice, + alias = process.slaAntiAlias || 1, + mark = Date.now(), + layermax = 0, + volume = 0; + + // filter ignored widgets + widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); + + // find max layer count + widgets.forEach(widget => { + layermax = Math.max(widget.slices.length); + }); + + let isPhoton = false; + + switch (device.deviceName) { + case 'Anycubic.Photon': + case 'Anycubic.Photon.S': + isPhoton = true; + break; + } + + if (isPhoton) { + let legacyMode = SLA.legacy || alias > 1, + part1 = legacyMode ? 0.25 : 0.85, + part2 = (1 - part1), + images = [], + slices = []; + + // generate layer bitmaps + // in wasm mode, rle layers generated here, too + let d = 8 / alias; + let masks = []; + for (let i=0; i { + online({progress: progress * part2 + part1, message}); + }); + ondone({ width, height, file, layers: images.length, volume }, [file]); + } else { + let part1 = 0.95; + let part2 = 1 - part1; + let slices = []; + + for (let index=0; index < layermax; index++) { + let param = { index, width, height, widgets, scaleX, scaleY }; + let { lines, area } = CXDLP.render(param); + volume += (area * layerZ); + slices.push(lines); + online({ + progress: (index / layermax) * part1, + message: "image_gen" + }); + // bail on an empty layer + if (lines.length === 0) { + break; + } + } + + // generate thumb, preview1, preview2 + let thumb = []; + let tdata = SLA.previewSmall.data; + for (let x=0; x<116; x++) { + for (let y=0; y<116; y++) { + let p = (x * 116 + y) * 4; + let r = (tdata[p + 0] >> 3) << 11; + let g = (tdata[p + 1] >> 2) << 5; + let b = (tdata[p + 2] >> 3); + let v = (r | g | b) & 0xffff; + thumb.push((v >> 8) & 0xff); + thumb.push(v & 0xff); + } + } + let preview1 = []; + let pdata = SLA.previewLarge.data; + for (let x=0; x<290; x++) { + for (let y=0; y<290; y++) { + let p = (x * 290 + y) * 4; + let r = (pdata[p + 0] >> 3) << 11; + let g = (pdata[p + 1] >> 2) << 5; + let b = (pdata[p + 2] >> 3); + let v = (r | g | b) & 0xffff; + preview1.push((v >> 8) & 0xff); + preview1.push(v & 0xff); + } + } + let preview2 = preview1; + + let file = CXDLP.export({ + settings, + width, + height, + slices, + thumb, + preview1, + preview2 + }); + ondone({ + width: width, + height: height, + file: file, + layers: slices.length, + volume + }, [file]); + } + + console.log('print.export', Date.now() - mark); +}; + +function generateCXDLP(print, conf, progress) { + console.log({generateCXDLP: print, conf, progress}); +} + + +export { generateCXDLP, widgets, isPhoton, legacyMode, d, masks, i, render, index, param, exp_func, file, part1, part2, slices, thumb, tdata, x, y, p, r, g, b, v, preview1, pdata, preview2 }; diff --git a/src2/kiri-mode/sla/slice.js b/src2/kiri-mode/sla/slice.js new file mode 100644 index 00000000..519694a3 --- /dev/null +++ b/src2/kiri-mode/sla/slice.js @@ -0,0 +1,704 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { base } from '../geo/base.js'; +import { point } from '../geo/point.js'; +import { polygon } from '../geo/polygon.js'; +import { polygons } from '../geo/polygons.js'; +import { slice } from '../kiri/slice.js'; +import { driver } from '../kiri-mode/sla/driver.js'; +import { pngjs } from '../ext/pngjs.js'; + + +const { base, kiri } = root; +const { driver, newSlice, newTop } = kiri; +const { util, newPoint, newPolygon, polygons } = base; +const { SLA } = driver; + +const POLY = polygons; +const FDM = driver.FDM.share; +const tracker = util.pwait; + +let fill_cache; + +/** + * DRIVER CONTRACT - runs in worker + * @param {Object} settings + * @param {Widget} Widget + * @param {Function} onupdate (called with % complete and optional message) + * @param {Function} ondone (called when complete with an array of Slice objects) + */ +SLA.slice = function(settings, widget, onupdate, ondone) { + let { process, device, controller } = settings, + isConcurrent = controller.threaded && kiri.minions.concurrent, + work_total, + work_remain; + + if (SLA.legacy && !self.OffscreenCanvas) { + return ondone("browser lacks support for OffscreenCanvas",true); + } + + // calculate % complete and call onupdate() + function doupdate(work, msg) { + onupdate(0.25 + ((work_total - work_remain) / work_total) * 0.75, msg); + work_remain -= work; + } + + // for each slice, perform a function and call doupdate() + function forSlices(slices, work, fn, msg) { + slices.forEach(function(slice,index) { + fn(slice,index); + doupdate(work / slices.length, msg) + }); + } + + let smallDims = { x: 200, y: 125 }; + let largeDims = { x: 400, y: 300 }; + + switch (device.deviceName) { + case 'Anycubic.Photon': + case 'Anycubic.Photon.S': + // defaults above + break; + case 'Creality.Halot.One': + case 'Creality.Halot.Max': + case 'Creality.Halot.Sky': + smallDims = { x: 116, y: 116 }; + largeDims = { x: 290, y: 290 }; + break; + } + + let sws = self.worker.snap.url; + let b64 = atob(sws.substring(sws.indexOf(',') + 1)); + let bin = Uint8Array.from(b64, c => c.charCodeAt(0)); + let img = new png.PNG(); + img.parse(bin, (err, data) => { + SLA.preview = img; + SLA.previewSmall = samplePNG(img, smallDims.x, smallDims.y); + SLA.previewLarge = samplePNG(img, largeDims.x, largeDims.y); + }); + let height = process.slaSlice || 0.05; + + async function onSliceDone(slices) { + // remove empty slices + slices = widget.slices = slices.filter(slice => slice.tops.length); + if (!process.slaOpenTop && !process.xray) { + // re-add last empty slice for closed top + let cap = newSlice(bounds.max.z + height); + cap.index = slices.last().index + 1; + slices.push(cap); + } + // prepend raft layers to slices array + if (process.slaSupportEnable && process.slaSupportLayers) { + let layers = process.slaSupportLayers, + zoff = height / 2, + snew = [], + polys = [], + gap = process.slaSupportGap, // gap layers above raft + grow = height, // union per layer expand + off = 1 - (layers * grow); // starting union offset from part + let outer = slices.forEach(slice => { + // poly.clone prevents inner voids from forming + polys.appendAll(slice.tops.map(t => t.poly.clone())); + }); + // p.clone prevents inner voids from forming + let union = POLY.union(polys, undefined, true).map(p => p.clone()); + let expand = POLY.expand(union, off, zoff, [], 1); + let lastraft; + for (let s=0; s { + slice.tops.push(newTop(u.clone(true).setZ(zoff))); + }); + expand = POLY.expand(expand, grow, zoff, [], 1); + lastraft = slice; + } + snew.push(slice); + zoff += height; + } + // compensate for midline start + zoff -= height / 2; + // replace slices with new appended array + slices = widget.slices = snew.concat(slices.map(s => { + s.tops.forEach(t => t.poly.setZ(s.z + zoff)); + s.index += snew.length; + s.z += zoff; + return s; + })); + // annotate widget for support generation + widget.union = union; + widget.lastraft = lastraft; + } + // re-connect slices into linked list for island/bridge projections + for (let i=1; i { return t+v }); + work_remain = work_total; + forSlices(slices, 5, (slice,index) => { + if (process.slaShell) { + FDM.doShells(slice, 2, 0, process.slaShell); + } else { + FDM.doShells(slice, 1, 0); + } + }, "shells"); + forSlices(slices, 10, (slice) => { + if (slice.synth) return; + FDM.doDiff(slice, 0.000001, { + sla: true, + fakedown: !process.slaOpenBase + }); + }, "delta"); + if (solidLayers) { + forSlices(slices, 10, (slice) => { + if (slice.synth) return; + FDM.projectFlats(slice, solidLayers); + FDM.projectBridges(slice, solidLayers); + }, "project"); + async function doUnionSolid(slice) { + if (slice.synth) return; + let traces = POLY.nest(POLY.flatten(slice.topShells())); + if (slice.solids) { + let trims = slice.solids || []; + traces.appendAll(trims); + // slice.unioned = POLY.setZ(POLY.union(traces, undefined, true), slice.z); + slice.unioned = POLY.setZ(await kiri.minions.union(traces), slice.z); + } else { + slice.unioned = traces; + } + } + let promises = slices.map(slice => doUnionSolid(slice)); + await tracker(promises, (i, t) => { + doupdate(10 / promises.length, "solid"); + }); + } else { + forSlices(slices, 10, (slice) => { + if (slice.synth) return; + slice.unioned = slice.topPolys(); + }, "solid"); + } + if (process.slaFillDensity && process.slaShell) { + fill_cache = []; + forSlices(slices, 60, (slice) => { + if (slice.synth) return; + fillPolys(slice, settings); + }, "infill"); + } + if (process.slaSupportEnable && process.slaSupportLayers && process.slaSupportDensity) { + computeSupports(widget, process, progress => { + doupdate(100 * progress, "support"); + }); + } + doRender(widget); + } + + let bounds = widget.getBoundingBox(); + let points = widget.getPoints(); + + base.slice(points, { + indices: process.indices || process.xray, + union: controller.healMesh, + debug: process.xray, + xray: process.xray, + zMin: bounds.min.z + height / 2, + zMax: bounds.max.z, + zInc: height, + // slicer function (worker local or minion distributed) + slicer(z, points, opts) { + return (isConcurrent ? kiri.minions.sliceZ : base.sliceZ)(z, points, opts); + }, + onupdate(v) { + return onupdate(0.0 + v * 0.25); + } + }).then(output => { + let slices = widget.slices = output.slices.map(data => { + let { z, lines, groups } = data; + let tops = POLY.nest(groups); + return newSlice(z).addTops(tops); + }); + onSliceDone(slices).then(ondone); + }); +}; + +function doRender(widget) { + widget.slices.forEach(slice => { + const render = slice.output(), lopacity = 0.6, line = 0x010101; + + if (slice.unioned) { + slice.unioned.forEach(poly => { + poly = poly.clone(true);//.move(widget.track.pos); + render + .setLayer("layers", { line, face: 0x0099cc, lopacity }) + .addAreas([poly], { outline: true }); + }); + } else if (slice.tops) { + slice.tops.forEach(top => { + let poly = top.poly;//.clone(true).move(widget.track.pos); + render + .setLayer("layers", { line, face: 0xfcba03, lopacity }) + .addAreas([poly], { outline: true }); + }); + } + + if (slice.supports) { + slice.supports.forEach(poly => { + render + .setLayer("support", { line, face: 0xfcba03, lopacity }) + .addAreas([poly], { outline: true }); + }); + } + }); +} + +function computeSupports(widget, process, progress) { + let area = widget.union.reduce((t,p) => { return t + p.areaDeep() }, 0), + perim = widget.union.reduce((t,p) => { return t + p.perimeter() }, 0), + slices = widget.slices, + length = slices.length, + tot_mass = 0, // total widget "mass" + tot_bear = 0; // total "mass-bearing" area + + let ops = slices.filter(slice => !slice.synth); + + // compute total "mass" by slice + ops.forEach(slice => { + slice.mass = slice.unioned.reduce((t,p) => { return t + p.areaDeep() }, 0); + if (slice.up && slice.up.bridges) { + slice.bear = slice.up.bridges.reduce((t,p) => { return t + p.areaDeep() }, 0); + slice.bear_up = slice.up.bridges; + tot_bear += slice.bear; + } else { + slice.bear = 0; + } + tot_mass += slice.mass; + }); + + let mass_per_bear = (tot_mass / tot_bear) * (1 / process.slaSupportDensity); + + // console.log({tot_mass, tot_bear, ratio: tot_mass / tot_bear, mass_per_bear}); + + let first; + ops.slice().map((s,i) => { + if (!first && s.bear) { + s.ord_first = first = true; + } + // 30x lowest to 1x highest + s.ord_weight = Math.pow(30,((ops.length - i) / ops.length)); + return s; + }).sort((a,b) => { + if (a.ord_first) return -1; + if (b.ord_first) return 1; + return (b.bear * b.ord_weight) - (a.bear * a.ord_weight); + }).forEach((slice, index) => { + slice.ord_bear = index; + }) + + let rem_mass = tot_mass, + rem_bear = tot_bear; + + // compute remaining mass, bearing surface, for each slice + let run, runLast, runCount = 0, runList = []; + ops.forEach(slice => { + slice.rem_mass = rem_mass; + slice.rem_bear = rem_bear; + // slice.can_bear = slice.mass * mass_per_bear; + slice.can_bear = slice.bear * mass_per_bear; + rem_mass -= slice.mass; + rem_bear -= slice.bear; + }); + + let ord = ops.sort((a,b) => { + return a.ord_bear - b.ord_bear; + }); + + rem_mass = tot_mass; + rem_bear = tot_bear; + + // in order of load bearing capability, select layer + // and recompute the requirements on the slices below + for (let i=0; iops[0].index; j--) { + slices[j].rem_mass -= bearing; + } + + // remove total mass left to bear + rem_mass -= bearing; + slice.can_emit = true; + if (rem_mass <= 0) { + // console.log({break: i, of: ord.length}); + break; + } + } + + let seq = 0, + seqLast = 0, + spacing = (1 - process.slaSupportDensity) * 10, + size = Math.bound(process.slaSupportSize / 2, 0.25, 1); + + // compute and project support pillars + slices.forEach((slice,index) => { + if (slice.can_emit) { + if (seq === 0 || slice.index - seqLast > 5) { + slice.bear_up.map(p => { + return p.clone(true).setZ(slice.z); + }).forEach(p => { + projectSupport(process, slice, p, size, spacing); + }); + seq++; + } else if (seq > 5) { + seq = 0; + } else { + seq++; + } + seqLast = slice.index; + } + progress(1 / slices.length); + }); + + // union support pillars + slices.forEach(slice => { + if (slice.supports) { + slice.supports = POLY.union(slice.supports, 0, true); + } + }); +} + +function projectSupport(process, slice, poly, size, spacing) { + let flat = poly.circularityDeep() > 0.1, + arr = [ poly ]; + + // insetting polys produces arrays. consume til gone + while (arr.length) { + poly = arr.shift(); + let out = [], + seg = [], + per = poly.perimeter(), + crit = 0, + polys = poly.clone(true).flattenTo([]); + + polys.forEach(p => p.forEachSegment((p1, p2) => { + let rec = {dist: p1.distTo2D(p2), p1, p2}; + if (rec.dist >= spacing || slice.ord_first) crit++; + seg.push(rec); + })); + + seg.sort((a,b) => { + return b.dist - a.dist; + }); + + // emit all critical (long) segments. min 3 + while (seg.length && (crit > 0 || out.length < 3)) { + let {dist, p1, p2} = seg.shift(); + if (dist >= spacing) { + // spaced along line + let num = Math.ceil(dist / spacing) + 1, + step = dist / num, + pt = p1; + while (num-- >= 0) { + out.push(pt); + pt = pt.offsetPointTo(p2, step); + } + crit--; + } else { + // line midpoint + out.push(p1.offsetPointTo(p2, dist / 2)); + } + } + + // drop points too close to other pillars + if (!slice.ord_first) + drop: for (let i=0; i { + if (out[i] && out[i].distTo2D(rec.point) <= size) { + out[i] = null; + } + }); + } + + // mark support pillar for each point + out.filter(p => p !== null) + .forEach(p => { + let track = projectPillar(process, slice, p, size, []); + // find stunted pillars terminating on a face (not raft or merged) + if (track.length && !(track.max || track.synth || track.merged)) { + // remove stunted pillar + track.forEach(rec => { + let sp = rec.slice.supports; + let rp = sp.indexOf(rec.pillar); + if (rp >= 0) sp.splice(rp,1); + }); + } + }); + + // inset polygon for flat area support + if (flat) poly.offset(Math.max(0.2, 1 - process.slaSupportDensity), arr); + } +} + +function projectPillar(process, slice, point, size, track) { + if (!slice.supports) slice.supports = []; + if (!slice.pillars) slice.pillars = []; + + let points = process.slaSupportPoints, + pillar = newPolygon() + .centerCircle(point, size/2, points, true) + .setZ(slice.z), + max = process.slaSupportSize, + inc = process.slaSlice/2, + end = false, // center intersects + over = [], // overlapping points + safe = [], // non-overlapping points + low = slice.index < process.slaSupportGap + process.slaSupportLayers; + + track.min = track.min ? Math.min(track.min, size) : size; + track.max = (track.max ? true : false) || size >= max; + track.synth = track.synth || slice.synth; + + slice.tops.forEach(t => { + if (track.length > 3 && point.isInPolygon(t.poly)) { + end = true; + } + pillar.points.forEach(p => { + let isin = p.isInPolygon(t.poly);// || p.nearPolygon(t.poly, 0.001); + if (isin) { + over.push(p); + } else { + safe.push(p); + } + }); + }); + + if (end) { + // backtrack shrinking pillars to point if + // not landing on the base and size is max'd + if (!slice.synth && track.max) { + let nusize = track.min; + while (track.length) { + let prec = track.pop(), + npil = newPolygon() + .centerCircle(prec.point, nusize/2, points, true) + .setZ(prec.slice.z), + spos = prec.slice.supports.indexOf(prec.pillar); + // if we find our old pillar, replace + if (spos >= 0) { + prec.slice.supports[spos] = npil; + nusize += inc; + } + if (nusize > prec.size) { + break; + } + } + } + return track; + } + + let nextpoint = point; + + if (over.length) { + // move toward average of safe (non-overlapping) points + if (safe.length) { + let x = 0, y = 0; + safe.forEach(p => { x += p.x; y += p.y }); + x /= safe.length; + y /= safe.length; + nextpoint = point.offsetPointTo({x, y, z:slice.z}, inc); + } + // once max'ed out, can only shrink in size + if (track.max) { + size -= inc; + } + } else if (!track.max || low) { + size += inc; + if (low) max += (process.slaSupportGap * process.slaSlice * 2); + } + + if (size < track.min) { + return track; + } + + // let close = []; + // for (let i=0; i { + // newp.x += p.x; + // newp.y += p.y; + // }); + // newp.x /= (close.length + 1); + // newp.y /= (close.length + 1); + // nextpoint = point.offsetPointTo(newp, inc); + // } + + slice.supports.push(pillar); + slice.pillars.push({point, pillar, size}); + track.push({slice, point, pillar, size}); + if (slice.down) { + projectPillar(process, slice.down, nextpoint, Math.min(size, max), track); + } + return track; +} + +function fillPolys(slice, settings) { + let process = settings.process, + device = settings.device, + polys = slice.unioned, + bounds = settings.bounds, + width = bounds.max.x - bounds.min.x, + depth = bounds.max.y - bounds.min.y, + max = Math.max(width,depth), + seq = Math.round(process.slaFillLine / process.slaSlice), + linew = process.slaFillLine, + units_w = (width / linew) * process.slaFillDensity, + units_d = (depth / linew) * process.slaFillDensity, + step_x = width / units_w, + step_y = depth / units_d, + start_x = -(width / 2), + start_y = -(depth / 2), + end_x = width / 2, + end_y = depth / 2, + fill = []; + + let seq_i = Math.floor(slice.index / seq), + seq_c = seq_i % 4, + cached = fill_cache[seq_c]; + + if (!cached && seq_c !== 1) + for (let x=start_x; x p.clone(true).setZ(slice.z)); + } + + fill = POLY.trimTo(fill, slice.tops.map(t => t.poly)); + fill = POLY.union(slice.unioned.appendAll(fill), 0, true); + + slice.unioned = fill; +} + +function pixAt(png,x,y) { + let idx = (x + png.width * y) * 4; + let dat = png.data; + return [ + dat[idx++], + dat[idx++], + dat[idx++], + dat[idx++] + ]; +} + +function averageBlock(png,x1,y1,x2,y2) { + let val = [0, 0, 0, 0], count = 0, x, y, z, v2; + for (x=x1; x= aspect) { + div = png.height / tw; + xoff = Math.round((png.width - (th * div)) / 2); + yoff = 0; + } else { + div = png.width / th; + xoff = 0; + yoff = Math.round((png.height - (tw * div)) / 2); + } + + for (let y=0; y png.height) continue; + ey = Math.round((y+1) * div + yoff); + for (let x=0; x png.width) continue; + ex = Math.round((x+1) * div + xoff); + bidx = (y * th + x) * 4; + pixval = averageBlock(png,dx,dy,ex,ey); + buf[bidx+0] = pixval[0]; + buf[bidx+1] = pixval[1]; + buf[bidx+2] = pixval[2]; + buf[bidx+3] = pixval[3]; + } + } + + return {width, height, data:buf, png}; +} + + +export { doupdate, forSlices, onSliceDone, doUnionSolid, doRender, computeSupports, projectSupport, projectPillar, fillPolys, pixAt, averageBlock, samplePNG, POLY, FDM, tracker, smallDims, largeDims, sws, b64, bin, img, height, cap, layers, outer, union, expand, s, slice, i, solidLayers, traces, trims, promises, bounds, points, slices, tops, render, poly, area, ops, mass_per_bear, rem_mass, ord, j, seq, flat, out, rec, num, track, sp, rp, isin, nusize, prec, nextpoint, x, close, p, d, newp, process, seq_i, y, idx, dat, th }; diff --git a/src2/kiri-mode/sla/x_cxdlp.js b/src2/kiri-mode/sla/x_cxdlp.js new file mode 100644 index 00000000..56588415 --- /dev/null +++ b/src2/kiri-mode/sla/x_cxdlp.js @@ -0,0 +1,452 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +const default_values = { + magic1: 'CXSW3DV2', + magic2: 'CXSW3DV2', + model: 'CL-89', + version: 1, + layer_count: 0, + res_x: 3840, + res_y: 2400, + height: 0, + thumb: undefined, + preview1: undefined, + preview2: undefined, + dim_x: '192.000000', + dim_y: '120.000000', + layer: '0.050000', + light_on: 6, + light_off: 2, + light_pwm: 255, + lift_dist: 6, + lift_speed: 60, + down_speed: 150, + base_layers: 8, + base_light_on: 60, + base_light_pwm: 255, + base_lift_dist: 5, + base_lift_speed: 60 +}; +const data_term = 0x0d0a; + +// https://github.com/sn4k3/UVtools/blob/v2.20.4/UVtools.Core/FileFormats/CXDLPFile.cs +// CL-60 1620 x 2560 +// CL-89 3840 x 2400 +// CL-133 3840 x 2160 + +class CXDLP { + constructor() { + Object.assign(this, default_values); + } + + // takes a DataView Object + read(view) { + let read = new DataReader(view, 0); + + function read_string(dbl) { + let len = read.readU32(); + let str = ''; + while (--len > 0) { + if (dbl) { + read.readU8(); + len--; + } + str += String.fromCharCode(read.readU8()); + } + if (!dbl) { + read.skip(1); // null term on byte strings + } + return str; + } + + let meta = { + view, + magic1: read_string(), + version: read.readU16(), + model: read_string(), + layer_count: read.readU16(), + res_x: read.readU16(), + res_y: read.readU16(), + height: read.readU32(), + skip: read.skip(60), + thumb: read.readBytes(26912), + skip: read.skip(2), // 0xd0a term + preview1: read.readBytes(168200), + skip: read.skip(2), // 0xd0a term + preview2: read.readBytes(168200), + skip: read.skip(2), // 0xd0a term + dim_x: read_string(true), + dim_y: read_string(true), + layer: read_string(true), + light_on: read.readU16(), + light_off: read.readU16(), + base_light_on: read.readU16(), + base_layers: read.readU16(), + base_lift_dist: read.readU16(), + base_lift_speed: read.readU16(), + lift_dist: read.readU16(), + lift_speed: read.readU16(), + down_speed: read.readU16(), + base_light_pwm: read.readU16(), + light_pwm: read.readU16() + }; + + // read layer record lengths + let layers = this.layers = []; + for (let i=0; i this.layers.length) { + throw `layer out of range 0-${this.layers.length}`; + } + let out = []; + let rec = this.layers[layer]; + let { lines, pos } = rec; + for (let j=0; j> 3; + let y_end = ((d1 & 0b111) << 10) | (d2 >> 6); + let x_end = ((d2 & 0b111111) << 8) | (d3 >> 8); + let color = d3 & 0xff; + out.push({y_start, y_end, x_end, color}); + } + return out; + } else { + throw "missing data view"; + } + } + + write() { + let output = new ArrayWriter(); + + output.write_string = function write_string(str, dbl = false) { + let len = str.length; + output.writeU32(dbl ? len * 2 : len + 1); + let pos = 0; + while (pos < len) { + if (dbl) { + output.writeU8(0); + } + output.writeU8(str.charCodeAt(pos++)); + } + if (!dbl) { + output.writeU8(0); + } + } + + output.write_bytes = function(array, len) { + if (!array) { + output.skip(len); + } else if (array.byteLength) { + if (len !== array.byteLength) { + throw `invalid array length: ${array.byteLength} != ${len}`; + } + // ArrayBuffer + let read = new DataReader(array); + for (let i=0; i> 5); + let b2 = ((line.y_start << 3) | (line.y_end >> 10)) & 0xff; + let b3 = (line.y_end >> 2) & 0xff; + let b4 = ((line.y_end << 6) | (line.x_end >> 8)) & 0xff; + let b5 = (line.x_end) & 0xff; + output.writeU8(b1); + output.writeU8(b2); + output.writeU8(b3); + output.writeU8(b4); + output.writeU8(b5); + output.writeU8(line.color); + } + layer.length = output.pos - start; + output.writeU16(data_term); + } + + output.write_string(this.magic2); + let ckpos = output.pos; + + // retrace and write layer lengths + for (let layer of layers) { + output.seek(layer.l1); + output.writeU32(layer.length); + output.seek(layer.l2); + output.writeU32(layer.length); + } + + output.seek(ckpos); + // compute xor checksum + let cksum = 0; + let array = output.array; + for (let i=0; i {return { lines: a }}); + cxdlp.res_x = device.resolutionX; + cxdlp.res_y = device.resolutionY; + cxdlp.dim_x = device.bedWidth.toFixed(5); + cxdlp.dim_y = device.bedDepth.toFixed(5); + cxdlp.layer = process.slaSlice.toFixed(5); + cxdlp.thumb = thumb; + cxdlp.preview1 = preview1; + cxdlp.preview2 = preview2; + cxdlp.light_on = process.slaLayerOn; + cxdlp.light_off = process.slaLayerOff; + // cxdlp.light_pwm = 255; + cxdlp.lift_dist = process.slaPeelDist; + cxdlp.lift_speed = process.slaPeelLiftRate * 60; + cxdlp.base_layers = process.slaBaseLayers; + cxdlp.base_light_on = process.slaBaseOn; + // cxdlp.base_light_pwm = 255; + cxdlp.base_lift_dist = process.slaBasePeelDist; + cxdlp.base_lift_speed = process.slaBasePeelLiftRate * 60; + + return cxdlp.write(); +}; + +CXDLP.render = function(params) { + let { width, height, index, widgets, scaleX, scaleY } = params; + let width2 = width / 2, height2 = height / 2; + let array = []; + let count = 0; + let area = 0; + + function scaleMovePoly(poly) { + let points = poly.points; + poly._bounds = undefined; + for (let i=0, il=points.length; i { + let slice = widget.slices[index]; + if (slice) { + if (slice.synth) count++; + let polys = slice.unioned; + if (!polys) polys = slice.tops.map(t => t.poly); + if (slice.supports) polys.appendAll(slice.supports); + array.appendAll(polys.map(poly => { + return poly.clone(true).move(widget.track.pos); + })); + count += polys.length; + } + }); + + let wasm = kiri.driver.SLA.wasm; + let imagelen = width * height; + let writer = new self.DataWriter(new DataView(wasm.memory.buffer), imagelen); + writer.writeU16(width, true); + writer.writeU16(height, true); + writer.writeU16(array.length, true); + + // scale and move all polys to fit in rendered platform coordinates + for (let i=0, il=array.length; i -- All Rights Reserved */ + +const { base, kiri } = root; +const { driver } = kiri; +const { util } = base; +const { SLA } = driver; + +function generatePhoton(print, conf, progress) { + let printset = print.settings, + process = printset.process, + device = printset.device, + width = conf.width, + height = conf.height, + layerCount = conf.lines.length, + layerBytes = width * height, + small = conf.small, + large = conf.large, + slices = conf.slices, + subcount = process.slaAntiAlias || 1, + masks = [], + coded; + + if (SLA.legacy || subcount > 1) { + let d = 8 / subcount; + for (let i=0; i { + let count = line.length; + let lineDV = new DataView(line.buffer); + let bits = new Uint8Array(line.length); + let bitsDV = new DataView(bits.buffer); + let subs = [{ data: bits, view: bitsDV }]; + for (let sl=1; sl { + progress(pro * 0.4 + 0.4, "layer_encode"); + })); + } else { + let codedlen = slices.reduce((t,l) => { + return t + l.reduce((t,a) => { + return t + a.length + }, 0); + }, 0); + coded = { + layers: slices.map(slice => { return { sublayers: slice }}), + length: codedlen + }; + } + + let codelen = coded.layers.length; + let buflen = 3000 + coded.length + (codelen * subcount * 28) + small.byteLength + large.byteLength; + let filebuf = new ArrayBuffer(buflen); + let filedat = new self.DataWriter(new DataView(filebuf)); + let printtime = (process.slaBaseLayers * process.slaBaseOn) + + (coded.layers.length - process.slaBaseLayers) * process.slaLayerOn; + + filedat.writeU32(0x1900fd12); // header + filedat.writeU32(2,true); // version + filedat.writeF32(68.04, true); // bed x + filedat.writeF32(120.96, true); // bed y + filedat.writeF32(150.0, true); // bed z + filedat.skip(12); // padding + filedat.writeF32(process.slaSlice, true); // layer height + filedat.writeF32(process.slaLayerOn, true); // default lamp on + filedat.writeF32(process.slaBaseOn, true); // base lamp on + filedat.writeF32(process.slaLayerOff, true); // lamp off + filedat.writeU32(process.slaBaseLayers, true); // base layers + filedat.writeU32(1440, true); // device x + filedat.writeU32(2560, true); // device y + let hirez = filedat.skip(4); // hirez preview address filled pater + let layerpos = filedat.skip(4); // layer data address filled later + filedat.writeU32(codelen, true); + let lorez = filedat.skip(4); // hirez preview address filled later + filedat.writeU32(printtime, true); // print time seconds + filedat.writeU32(1, true); // projection type (1=lcd, 0=cast) + let proppos = filedat.skip(4); // print properties address filled later + let proplen = filedat.skip(4); // print properties length filled later + filedat.writeU32(subcount, true); // AA level (sub layers) + filedat.writeU16(0x00ff, true); // light pwm (TODO); + filedat.writeU16(0x00ff, true); // light pwm bottom (TODO); + + let propstart = filedat.pos; + filedat.view.setUint32(proppos, filedat.pos, true); + // write print properties + filedat.writeF32(process.slaBasePeelDist, true); + filedat.writeF32(process.slaBasePeelLiftRate * 60 , true); + filedat.writeF32(process.slaPeelDist, true); + filedat.writeF32(process.slaPeelLiftRate * 60 , true); + filedat.writeF32(process.slaPeelDropRate * 60, true); + filedat.writeF32(0, true); // volume of used + filedat.writeF32(0, true); // weight of used + filedat.writeF32(0, true); // cost of used + filedat.writeF32(0, true); // bottom off delay time + filedat.writeF32(0, true); // light off delay time + filedat.writeU32(process.slaBaseLayers, true); + filedat.writeF32(0, true); // p1 ? + filedat.writeF32(0, true); // p2 ? + filedat.writeF32(0, true); // p3 ? + filedat.writeF32(0, true); // p4 ? + filedat.view.setUint32(proplen, filedat.pos - propstart, true); + + filedat.view.setUint32(layerpos, filedat.pos, true); + // write layer headers + let layers = coded.layers; + let layerat = []; + + for (let sc=0; sc { + let count = line.length / 4; + let bits = new Uint8Array(line.length / 4); + let bitsDV = new DataView(bits.buffer); + let lineDV = new DataView(line.buffer); + // reduce RGB to R = 0||1 + for (let i = 0; i < count; i++) { + // defeat anti-aliasing for the moment + bitsDV.setUint8(i, lineDV.getUint8(i * 4) > 0 ? 1 : 0); + } + progress(index / conf.lines.length); + return { subs: [{ + exposureTime: process.slaLayerOn, + data: bits + }] }; + }); + coded = encodeLayers(converted, "photons"); + } else { + let codedlen = slices.reduce((t,l) => { + return t + l.reduce((t,a) => { + return t + a.length + }, 0); + }, 0); + coded = { + layers: slices.map(slice => { return { sublayers: slice }}), + length: codedlen + }; + } + + let filebuf = new ArrayBuffer(75366 + coded.length + 28 * layerCount); + let filedat = new DataView(filebuf); + let filePos = 0; + + filedat.setUint32 (0, 2, false); + filedat.setUint32 (4, 3227560, false); + filedat.setUint32 (8, 824633720, false); + filedat.setUint16 (12, 10, false); + filedat.setFloat64(14, process.slaSlice, false); + filedat.setFloat64(22, process.slaLayerOn, false); + filedat.setFloat64(30, process.slaLayerOff, false); + filedat.setFloat64(38, process.slaBaseOn, false); + filedat.setUint32 (46, process.slaBaseLayers, false); + filedat.setFloat64(50, process.slaPeelDist, false); + filedat.setFloat64(58, process.slaPeelLift, false); + filedat.setFloat64(66, process.slaPeelDrop, false); + filedat.setFloat64(74, 69420, false); + filedat.setUint32 (82, 224, false); + filedat.setUint32 (86, 42, false); + filedat.setUint32 (90, 168, false); + filedat.setUint32 (94, 10, false); + filedat.setUint32 (75362, layerCount, false); + + filePos = 75366; + for (let i = 0; i < layerCount; i++) { + let layer = coded.layers[i], + sublayer = layer.sublayers[0], + numbytes = sublayer.length; + + filedat.setUint32 (filePos + 0, 69420, false); + filedat.setFloat64(filePos + 4, 0); + filedat.setUint32 (filePos + 12, height, false); + filedat.setUint32 (filePos + 16, width, false); + filedat.setUint32 (filePos + 20, numbytes * 8 + 32, false); + filedat.setUint32 (filePos + 24, 2684702720, false); + filePos += 28; + for (let j = 0; j < numbytes; j++) { + filedat.setUint8(filePos + j, sublayer[j], false); + } + filePos += numbytes; + progress((i / layerCount) / 2 + 0.5); + } + + return filebuf; +} + +function encodeLayers(input, type, progress) { + let layers = [], length = 0, total = 0, count = 0; + input.forEach(layer => { + layer.subs.forEach(sub => total++); + }); + for (let index = 0; index < input.length; index++) { + let subs = input[index].subs, + sublayers = [], + sublength = 0; + for (let subindex = 0; subindex < subs.length; subindex++) { + let data = subs[subindex].data; + let encoded = rleEncode(data, type); + sublength += encoded.length; + sublayers.push(encoded); + if (progress) progress(count++/total); + if (type == "photons") break; + } + length += sublength; + layers.push({ + sublength, + sublayers + }); + } + return { length, layers }; +} + +function rleEncode(data, type) { + let maxlen = (type === 'photons') ? 128 : 125, + color = data[0], + runlen = 1, + output = []; + for (let index = 1; index < data.length; index++) { + let newColor = data[index]; + if (newColor !== color) { + output.push(rleByte(color, runlen, type)); + color = newColor; + runlen = 1; + } else { + if (runlen === maxlen) { + output.push(rleByte(color, runlen, type)); + runlen = 1; + } else { + runlen++; + } + } + } + if (runlen > 0) { + output.push(rleByte(color, runlen, type)); + } + return output; +} + +function rleByte(color, length, type) { + switch (type) { + case 'photon': + return (length & 0x7f) | ((color << 7) & 0x80); + case 'photons': + length--; + return (length & 1 ? 128 : 0) | + (length & 2 ? 64 : 0) | + (length & 4 ? 32 : 0) | + (length & 8 ? 16 : 0) | + (length & 16 ? 8 : 0) | + (length & 32 ? 4 : 0) | + (length & 64 ? 2 : 0) | color; + } +} + +function rleDecode(data, type) { + let bytes = []; + if (type === 'photon') { + for (let i = 0; i < data.length; i++) { + let val = data[i], + color = val >> 7, + count = val & 0x7f; + for (let j = 0; j < count; j++) { + bytes.push(color); + } + } + } else { + for (let i = 0; i < data.length; i++) { + let val = data[i], + color = val & 1, + count = + ((val & 128 ? 1 : 0) | + (val & 64 ? 2 : 0) | + (val & 32 ? 4 : 0) | + (val & 16 ? 8 : 0) | + (val & 8 ? 16 : 0) | + (val & 4 ? 32 : 0) | + (val & 2 ? 64 : 0)) + 1; + for (let j = 0; j < count; j++) { + bytes.push(color); + } + } + } + return bytes; +} + +// write out a thumbnail image +function writePhotonImage(preview, writer) { + let data = new Uint8Array(preview.data), len = data.byteLength; + writer.writeU32(preview.width, true); + writer.writeU32(preview.height, true); + let hpos = writer.skip(4); + writer.writeU32(len/2, true); + writer.view.setUint32(hpos, writer.pos, true); + let pos = 0; + while (pos < len) { + let r = data[pos++], + g = data[pos++], + b = data[pos++], + a = data[pos++], + v = (((r/4)&0x1f) << 11) | + (((g/4)&0x1f) << 6) | + (((b/4)&0x1f) << 0) ; + writer.writeU16(v, true); + } +} + +// for unbound workers +// if (self.WASM) { +// let {exports} = wasmInstance; +// let heap = new Uint8Array(exports.memory.buffer); +// self.wasm = { +// heap, +// memory: exports.memory, +// // heap: wasmMemory, +// // memory: memoryBytes, +// render: exports.render, +// rle_encode: exports.rle_encode +// }; +// } else + +// new WebAssembly rasterizer +function renderLayerWasm(params) { + let { width, height, index, widgets, scaleX, scaleY, masks } = params; + let width2 = width / 2, height2 = height / 2; + let array = []; + let count = 0; + let area = 0; + + function scaleMovePoly(poly) { + let points = poly.points; + poly._bounds = undefined; + for (let i=0, il=points.length; i { + let slice = widget.slices[index]; + if (slice) { + if (slice.synth) count++; + let polys = slice.unioned; + if (!polys) polys = slice.tops.map(t => t.poly); + if (slice.supports) polys.appendAll(slice.supports); + array.appendAll(polys.map(poly => { + return poly.clone(true).move(widget.track.pos); + })); + count += polys.length; + } + }); + + let wasm = SLA.wasm; + let imagelen = width * height; + let writer = new self.DataWriter(new DataView(wasm.memory.buffer), imagelen); + writer.writeU16(width, true); + writer.writeU16(height, true); + writer.writeU16(array.length, true); + + // scale and move all polys to fit in rendered platform coordinates + for (let i=0, il=array.length; i { + let slice = widget.slices[index]; + if (slice) { + // prevent premature exit on empty synth slice + if (slice.synth) count++; + let polys = slice.unioned; + if (!polys) polys = slice.tops.map(t => t.poly); + if (slice.supports) polys.appendAll(slice.supports); + polys.forEach(poly => { + area += poly.areaDeep(); + poly.move(widget.track.pos); + ctx.beginPath(); + polyout(poly.setClockwise(), ctx, opt); + if (poly.inner) { + poly.inner.forEach(inner => { + polyout(inner.setCounterClockwise(), ctx, opt); + }); + } + ctx.fill(); + count++; + }); + } else { + // console.log({no_slice_at: index}) + } + }); + let data = ctx.getImageData(0,0,height,width).data; + // reduce RGBA to R + let red = new Uint8ClampedArray(data.length / 4); + for (let i=0; i { + if (i === 0) { + ctx.moveTo(height - (p.y * scaleY + height2), p.x * scaleX + width2); + } else { + ctx.lineTo(height - (p.y * scaleY + height2), p.x * scaleX + width2); + } + }, true); + ctx.closePath(); +} + +self.photon = { + generatePhoton, + generatePhotons, + renderLayer, + renderLayerWasm +}; + + +export { generatePhoton, generatePhotons, encodeLayers, rleEncode, rleByte, rleDecode, writePhotonImage, renderLayerWasm, scaleMovePoly, writePoly, renderLayer, polyout, printset, d, i, ccl, tcl, converted, count, lineDV, bits, bitsDV, subs, sl, s, view, mask, dv, codedlen, codelen, buflen, filebuf, filedat, printtime, hirez, layerpos, lorez, proppos, proplen, propstart, layers, layerat, sc, l, layer, clo, tlo, j, filePos, index, subindex, data, encoded, maxlen, newColor, bytes, hpos, pos, r, heap, width2, array, area, points, p, inner, bounds, point, slice, polys, wasm, imagelen, writer, poly, image, rlelen, opt, ctx, red }; diff --git a/src2/kiri-mode/wedm/driver.js b/src2/kiri-mode/wedm/driver.js new file mode 100644 index 00000000..627aa0ae --- /dev/null +++ b/src2/kiri-mode/wedm/driver.js @@ -0,0 +1,129 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { driver } from '../kiri-mode/laser/driver.js'; + + +const { kiri, moto } = root; +const DRIVERS = kiri.driver; +const { CAM, LASER } = DRIVERS; +const { TYPE } = LASER; +const WEDM = DRIVERS.WEDM = Object.assign({}, LASER, { + name: "WireEDM", + type: TYPE.WEDM, + init, + prepare +}); +const DEG2RAD = Math.PI / 180; + +const state = { + api: undefined, + alert: undefined, + radians: 0, + surfaces: [], + selecting: false +}; + +function faceAdd() { + if (state.selecting) return faceDone(); + + const { api, radians, surfaces } = state; + CAM.surface_prep(radians, () => { + api.hide.alert(alert); + state.alert = api.show.alert("[esc] cancels surface selection"); + for (let [wid, arr] of Object.entries(surfaces)) { + let widget = api.widgets.forid(wid); + if (widget && arr.length) + for (let faceid of arr) { + CAM.surface_toggle(widget, faceid, radians, faceids => { + // surfaces[widget.id] = faceids; + }); + } + } + api.feature.on_mouse_up = (obj, ev) => { + let { face } = obj; + let min = Math.min(face.a, face.b, face.c); + let faceid = min / 3; + let widget = obj.object.widget; + CAM.surface_toggle(widget, faceid, radians, faceids => { + surfaces[widget.id] = faceids; + }); + }; + state.selecting = true; + }); +} + +function faceDone() { + if (!state.selecting) return; + + const { api, alert, surfaces } = state; + for (let wid of Object.keys(surfaces)) { + let widget = api.widgets.forid(wid); + if (widget) { + CAM.surface_clear(widget); + } else { + delete surfaces[wid]; + } + } + api.hide.alert(alert); + api.feature.on_mouse_up = undefined; + api.ui.faceAdd.classList.remove('editing') + state.selecting = false; +} + +function init(kiri, api, driver) { + LASER.init(kiri, api, driver); + + state.api = api; + + api.event.on("key.esc", () => { + if (state.selecting) { + faceDone(); + } + }); + + // api.event.on("mode.set", (mode) => { }); + + // api.event.on("view.set", (mode) => { }); + + api.event.on("preview.view", view => { + const process = api.conf.get().process; + if (!process.ctOriginBounds) return; + const bounds = new THREE.Box3().expandByObject(view); + api.platform.set_bounds(new THREE.Box3( + new THREE.Vector3(bounds.min.x, bounds.min.z, bounds.min.y), + new THREE.Vector3(bounds.max.x, bounds.max.z, bounds.max.y), + )); + }); + + // Surface Selection Buttons + api.event.on("button.click", target => { + let process = api.conf.get().process; + switch (target) { + case api.ui.faceAdd: + target.classList.add('editing'); + return faceAdd(); + case api.ui.faceDun: + return faceDone(); + case api.ui.faceClr: + api.uc.confirm("clear surface selection?").then(ok => { + // todo + }); + break; + } + }); + +}; + +async function prepare(widgets, settings, update) { + // let device = settings.device, + // process = settings.process, + // print = self.worker.print = kiri.newPrint(settings, widgets), + // output = print.output = []; + + // filter ignored widgets + widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); + + LASER.prepare(widgets, settings, update); +} + + +export { faceAdd, faceDone, init, prepare, DRIVERS, WEDM, DEG2RAD, state, widget, min, faceid, process, bounds, device }; diff --git a/src2/kiri-mode/wjet/driver.js b/src2/kiri-mode/wjet/driver.js new file mode 100644 index 00000000..f20f1816 --- /dev/null +++ b/src2/kiri-mode/wjet/driver.js @@ -0,0 +1,11 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { driver } from '../kiri-mode/laser/driver.js'; + + +const DRIVERS = root.kiri.driver; +const { LASER } = DRIVERS; +const { TYPE } = LASER; +DRIVERS.WJET = Object.assign({}, LASER, { name: 'WaterJet', type: TYPE.WJET }); + + +export { DRIVERS }; diff --git a/src2/kiri-run/cli.js b/src2/kiri-run/cli.js new file mode 100644 index 00000000..022e6810 --- /dev/null +++ b/src2/kiri-run/cli.js @@ -0,0 +1,227 @@ +/** example of how to use Kiri:Moto's slicer engine from the command-line */ +let fs = require('fs'); +let args = process.argv.slice(2); +let opts = { + dir: process.cwd(), + output: "-", + model: "web/obj/cube.stl", + source: "src/cli/kiri-source.json", + controller: "src/cli/kiri-controller.json", + process: "src/cli/kiri-fdm-process.json", + device: "src/cli/kiri-fdm-device.json", + tools: "src/cli/kiri-cam-tools.json" +}; +for (let i=0; i ", + " --verbose | enable verbose logging", + " --dir=[dir] | root directory for file paths (default: '.')", + " --model=[file] | model file to load (or last parameter)", + " --tools=[file] | tools array for CAM mode", + " --source=[file] | source file list (defaults to kiri engine)", + " --device=[file] | device definition file (json)", + " --process=[file] | process definition file (json)", + " --controller=[file] | controller definition file (json)", + " --output=[file] | gcode output to file or '-' for stdout", + " --position=x,y,z | move loaded model to position x,y,z", + " --rotate=x,y,z | rotate loaded model x,y,z radians", + " --scale=x,y,z | scale loaded model in x,y,z", + " --move=x,y,z | move loaded model x,y,z millimeters" + ].join("\r\n")); + return; +} + +let { dir, verbose, model, output, position, move, scale, rotate } = opts; +let exports_save = exports, + navigator = { userAgent: "" }, + module_save = module, + THREE = {}, + gapp = {}, + geo = {}, + noop = () => { }, + self = this.self = { + gapp, + THREE, + location: { hostname: 'local', port: 0, protocol: 'fake' }, + postMessage: (msg) => { + self.kiri.client.onmessage({data:msg}); + } + }; + +// fake fetch for worker to get wasm, if needed +let fetch = function(url, opts = {}) { + if (verbose) console.log({fetch: url}); + if (!url.startsWith('/')) { + url = `${dir}/${url}`; + } + let buf = fs.readFileSync(url); + return new Promise((resolve, reject) => { + resolve(new Promise((resolve, reject) => { + if (opts.format === 'string') { + return resolve(buf.toString()); + } + if (opts.format === 'buffer') { + return resolve(buf); + } + if (opts.format === 'eval') { + return resolve(eval('(' + buf + ')')); + } + resolve({ + arrayBuffer: function() { + return buf; + } + }); + })); + }); +}; + +// imitate worker process +class Worker { + constructor(url) { + if (verbose) console.log({worker: url}); + } + + postMessage(msg) { + setImmediate(() => { + self.kiri.worker.onmessage({data:msg}); + }); + } + + onmessage(msg) { + // if we end up here, something went wrong + console.trace('worker-recv', msg); + } + + terminate() { + // if we end up here, something went wrong + console.trace('worker terminate'); + } +} + +// node is missing these functions so put them in scope during eval +function atob(a) { + return Buffer.from(a).toString('base64'); +} + +function btoa(b) { + return Buffer.from(b, 'base64').toString(); +} + +async function run() { + let files = await fetch(opts.source, { format: "eval" } ); + let tools = await fetch(opts.tools, { format: "eval" } ); + let device = await fetch(opts.device, { format: "eval" } ); + let process = await fetch(opts.process, { format: "eval" } ); + + for (let file of files.map(p => `${dir}/src/${p}.js`)) { + let isPNG = file.indexOf("/pngjs") > 0; + let isClip = file.indexOf("/clip") > 0; + let isEarcut = file.indexOf("/earcut") > 0; + let isTHREE = file.indexOf("/three") > 0; + if (isTHREE) { + // THREE.js kung-fu fake-out + exports = {}; + } + let swapMod = isEarcut; + if (swapMod) { + module = { exports: {} }; + } + let clearMod = isPNG || isClip; + if (clearMod) { + module = undefined; + } + try { + if (verbose) console.log(`loading ... ${file}`); + eval(fs.readFileSync(`${file}`).toString()); + } catch (e) { + throw e; + } + if (isClip) { + ClipperLib = self.ClipperLib; + } + if (isTHREE) { + Object.assign(THREE, exports); + // restore exports after faking out THREE.js + exports = exports_save; + } + if (isEarcut) { + self.earcut = module.exports; + } + if (clearMod || swapMod) { + module = module_save; + } + } + + let { kiri, moto, load } = self; + + console.log({version: kiri.version}); + + let engine = kiri.newEngine(); + let data = await fetch(model) + let buf = new Uint8Array(data.arrayBuffer()).buffer; + + return engine.parse(buf) + .then(data => { if (verbose) console.log({loaded: data}) }) + .then(() => { + if (position) { + let [ x, y, z ] = position.split(',').map(v => parseFloat(v || 0)); + if (verbose) console.log('moveTo', {x, y, z}); + engine.moveTo(x,y,z); + } + if (move) { + let [ x, y, z ] = move.split(',').map(v => parseFloat(v || 0)); + if (verbose) console.log('move', {x, y, z}); + engine.move(x,y,z); + } + if (scale) { + let [ x, y, z ] = scale.split(',').map(v => parseFloat(v || 0)); + if (verbose) console.log('scale', {x, y, z}); + engine.scale(x,y,z); + } + if (rotate) { + let [ x, y, z ] = rotate.split(',').map(v => parseFloat(v || 0)); + if (verbose) console.log('rotate', {x, y, z}); + engine.rotate(x,y,z); + } + }) + .then(() => engine.setDevice(device)) + .then(() => engine.setProcess(process)) + .then(() => { if (device.mode === 'CAM') engine.setTools(tools) }) + .then(() => engine.setMode(device.mode)) + .then(eng => eng.slice()) + .then(eng => eng.prepare()) + .then(eng => engine.export()) + .then(gcode => { + if (output === '-') { + console.log({gcode}); + } else { + fs.writeFileSync(output, gcode); + } + }) + .catch(error => { + console.log({error}); + }); +} + +run(); diff --git a/src2/kiri-run/engine.js b/src2/kiri-run/engine.js new file mode 100644 index 00000000..4ee2b2a9 --- /dev/null +++ b/src2/kiri-run/engine.js @@ -0,0 +1,222 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// Future imports for src/kiri-run/engine.js when unwrapped from gapp.register() + +// Dependencies from dep: comments +// import { moto_license } from 'moto/license.js'; +// import { load_stl } from 'load/stl.js'; +// import { kiri_conf } from 'kiri/conf.js'; +// import { kiri_client } from 'kiri/client.js'; +// import { kiri_widget } from 'kiri/widget.js'; + +// Dependencies from use: comments +// import { add_three } from 'add/three.js'; +// import { add_array } from 'add/array.js'; + +// Core dependencies from root +import { kiri } from '../../main/gapp.js'; + +// Note: This module also depends on: +// - moto.license (for licensing functionality) +// - load.stl (for STL file loading) +// - kiri.conf (for configuration) +// - kiri.client (for client functionality) +// - kiri.widget (for widget functionality) +// - add.three (for Three.js utilities) +// - add.array (for array utilities) +// - Various kiri API functions injected via root + +class Engine { + constructor() { + this.widget = kiri.newWidget(); + this.settings = { + mode: "FDM", + controller: {}, + render: false, + filter: { FDM: "internal" }, + device: kiri.conf.defaults.fdm.d, // device profile + process: kiri.conf.defaults.fdm.p, // slicing settings + widget: { [ this.widget.id ]: {} } + }; + this.listener = () => {}; + } + + load(url) { + return new Promise((accept, reject) => { + try { + new load.STL().load(url, vertices => { + this.listener({loaded: url, vertices}); + this.widget.loadVertices(vertices).center(); + accept(this); + }); + } catch (error) { + reject(error); + } + }); + } + + clear() { + kiri.api.platform.clear(); + } + + parse(data) { + return new Promise((accept, reject) => { + try { + let vertices = new load.STL().parse(data); + this.listener({parsed: data, vertices}); + this.widget.loadVertices(vertices).center(); + accept(this); + } catch (error) { + reject(error); + } + }); + } + + setListener(listener) { + this.listener = listener; + return this; + } + + setRender(bool) { + this.settings.render = bool; + return this; + } + + /** + * Sets the mode of the engine. Valid modes are: + * @param {"FDM"|"CAM"|"LASER"|"SLA"} mode - the mode to set + * @returns {Engine} this + */ + setMode(mode) { + this.settings.mode = mode; + return this; + } + + setDevice(device) { + Object.assign(this.settings.device, device); + return this; + } + + setProcess(process) { + Object.assign(this.settings.process, process); + return this; + } + + setController(controller) { + let ctrl = this.settings.controller; + Object.assign(ctrl, controller); + if (ctrl.threaded) { + kiri.client.pool.start(); + } else { + kiri.client.pool.stop(); + } + return this; + } + + setTools(tools) { + this.settings.tools = tools; + return this; + } + + setStock(stock) { + let { settings } = this; + let { process } = settings; + settings.stock = stock; + process.camStockX = stock.x; + process.camStockY = stock.y; + process.camStockZ = stock.z; + return this; + } + + setOrigin(x, y, z) { + this.settings.origin = { x, y, z }; + return this; + } + + moveTo(x, y, z) { + this.widget.move(x, y, z, true); + return this; + } + + move(x, y, z) { + this.widget.move(x, y, z); + return this; + } + + scale(x, y, z) { + this.widget.scale(x, y, z); + return this; + } + + rotate(x, y, z) { + this.widget.rotate(x, y, z); + return this; + } + + slice() { + return new Promise((accept, reject) => { + kiri.client.clear(); + kiri.client.sync([ this.widget ]); + kiri.client.rotate(this.settings); + kiri.client.slice(this.settings, this.widget, msg => { + this.listener({slice:msg}); + if (msg.error) { + reject(msg.error); + } + if (msg.done) { + accept(this); + } + }); + }); + } + + prepare() { + return new Promise((accept, reject) => { + kiri.client.prepare(this.settings, update => { + this.listener({prepare:{update}}); + }, done => { + this.listener({prepare:{done:true}}); + accept(this); + }); + }); + } + + export() { + return new Promise((accept, reject) => { + let output = []; + kiri.client.export(this.settings, segment => { + if (typeof segment === 'string') { + this.listener({export:{segment}}); + output.push(segment); + } + }, done => { + this.listener({export:{done}}); + accept(output.join('\r\n')); + }); + }); + } +} + +function newEngine() { + return new Engine(); +} + +gapp.overlay(kiri, { + newEngine +}); + +// Future exports for src/kiri-run/engine.js when unwrapped from gapp.register() + +// Main Engine class +export { Engine }; + +// Factory function +export { newEngine }; + +// Default export - the main engine functionality +export default { + Engine, + newEngine +}; \ No newline at end of file diff --git a/src2/kiri-run/frame.js b/src2/kiri-run/frame.js new file mode 100644 index 00000000..02d06e62 --- /dev/null +++ b/src2/kiri-run/frame.js @@ -0,0 +1,89 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +(function() { + let KIRI = self.kiri = self.kiri || {}; + let frame, cwin, onevent = {}, targetOrigin; + + function $(id) { return document.getElementById(id) } + + function setFrame(fo) { + frame = fo; + cwin = fo.contentWindow; + } + + function send(msg) { + cwin.postMessage(msg, targetOrigin); + } + + function recv(msg) { + let { origin, source, target, data } = msg; + let efn = onevent[data.event]; + if (efn) { + efn(data.data, data.event); + } + if (API.onmessage) { + API.onmessage(data, msg); + } + } + + let API = KIRI.frame = { + setFrame: (io, target) => { + let type = typeof(io); + targetOrigin = target; + switch (type) { + case 'string': setFrame($(io)); break; + case 'object': setFrame(io); break; + default: throw `invalid frame type ${type}`; + } + window.addEventListener('message', msg => { + let { origin, source, target, data } = msg; + if (source.window === cwin) { + recv(msg); + } + }); + }, + + send: send, + + load: (load) => { send({ load }) }, + + clear: () => { send({ clear: true }) }, + + parse: (data, type) => { send({ parse: data, type })}, + + get: (scope) => { send({ get: scope })}, + + setMode: (mode) => { send({ mode }) }, + + setDevice: (device) => { send({ device })}, + + setProcess: (process) => { send({ process })}, + + setFeatures: (features) => { send({ features })}, + + setController: (controller) => { send({ controller })}, + + slice: () => { send({ function: "slice", callback: true }) }, + + prepare: () => { send({ function: "prepare", callback: true }) }, + + export: (cb) => { send({ function: "export", callback: cb ? true : false }) }, + + onmessage: () => { }, + + progress: (progress, message) => { send({ progress, message }) }, + + alert: (alert, time) => { send({ alert, time }) }, + + emit: (emit, message) => { send({ emit, message }) }, + + onevent: (event, fn) => { + onevent[event] = fn; + send({ event }); + } + }; + + API.on = API.onevent; +})(); diff --git a/src2/kiri-run/minion.js b/src2/kiri-run/minion.js new file mode 100644 index 00000000..1976752c --- /dev/null +++ b/src2/kiri-run/minion.js @@ -0,0 +1,194 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// Future imports for src/kiri-run/minion.js when unwrapped from gapp.register() + +// Core dependencies from root +import { base, kiri } from '../../main/gapp.js'; + +// Base dependencies +import { polygons } from '../../geo/base.js'; + +// Kiri dependencies +import { codec } from '../../kiri/index.js'; + +// Moto dependencies +import { broker } from '../../moto/broker.js'; + +// Note: This module also depends on: +// - moto.license (for licensing functionality) +// - geo.slicer (for slicing functionality) +// - geo.wasm (for WebAssembly functionality) +// - kiri-mode.fdm.post (for FDM post processing) +// - kiri-mode.cam.topo (for CAM topo functionality) +// - kiri-mode.cam.topo4 (for CAM topo4 functionality) +// - kiri-mode.cam.slicer (for CAM slicing) +// - ext.clip2 (for clipping operations) +// - Various kiri API functions injected via root + +const POLY = polygons; +const clib = self.ClipperLib; +const ctyp = clib.ClipType; +const ptyp = clib.PolyType; +const cfil = clib.PolyFillType; + +let cache = self.cache = {}; +let name = "unknown"; + +// catch clipper alerts and convert to console messages +self.alert = function(o) { + console.log(o); +}; + +self.onmessage = function(msg) { + let data = msg.data; + let cmd = data.cmd; + (funcs[cmd] || funcs.bad)(data, data.seq, cmd); +}; + +function reply(msg, direct) { + self.postMessage(msg, direct); +} + +function log() { + console.log(`[${name}]`, ...arguments); +} + +const funcs = self.minion = { + label(data, seq) { + name = data.name; + }, + + config: data => { + if (data.base) { + Object.assign(base.config, data.base); + } else { + log({invalid: data}); + } + }, + + union: (data, seq) => { + if (!(data.polys && data.polys.length)) { + reply({ seq, union: codec.encode([]) }); + return; + } + let state = { zeros: [] }; + let polys = codec.decode(data.polys); + let union = POLY.union(polys, data.minarea || 0, true); + reply({ seq, union: codec.encode(union) }, state.zeros); + }, + + topShells: (data, seq) => { + let top = codec.decode(data.top, {full: true}); + let {z, count, offset1, offsetN, fillOffset, opt} = data; + kiri.driver.FDM.doTopShells(z, top, count, offset1, offsetN, fillOffset, opt); + let state = { zeros: [] }; + reply({ seq, top: codec.encode(top, {full: true}) }, state.zeros); + }, + + fill: (data, seq) => { + let polys = codec.decode(data.polys); + let { angle, spacing, minLen, maxLen } = data; + let fill = POLY.fillArea(polys, angle, spacing, [], minLen, maxLen); + let arr = new Float32Array(fill.length * 4); + for (let i=0, p=0; p { + const clip = new clib.Clipper(); + const ctre = new clib.PolyTree(); + const clips = []; + const M = base.config.clipper; + + let lines = data.lines.map(array => { + return codec.decodePointArray2D(array, data.z, (X, Y) => { return {X: X*M, Y: Y*M} }) + }); + let polys = data.polys.map(array => { + return codec.decodePointArray2D(array, data.z, (X, Y) => { return {X: X*M, Y: Y*M} }) + }); + + clip.AddPaths(lines, ptyp.ptSubject, false); + clip.AddPaths(polys, ptyp.ptClip, true); + + const state = { zeros: [] }; + if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { + for (let node of ctre.m_AllPolys) { + clips.push(codec.encode(POLY.fromClipperNode(node, data.z), state.zeros)); + } + } + + reply({ seq, clips }, state.zeros); + }, + + sliceZ: (data, seq) => { + let { z, points, options } = data; + let i = 0, p = 0, realp = new Array(points.length / 3); + while (i < points.length) { + realp[p++] = base.newPoint(points[i++], points[i++], points[i++]).round(3); + } + let state = { zero: [] }; + let output = []; + base.sliceZ(z, realp, { + ...options, + each(out) { output.push(out) } + }).then(() => { + for (let rec of output) { + // lines do not pass codec properly (for now) + delete rec.lines; + } + reply({ seq, output: codec.encode(output) }, state.zeros); + }); + }, + + putCache: msg => { + const { key, data } = msg; + // log({ minion_putCache: key, data }); + if (data) { + cache[key] = data; + } else { + delete cache[key]; + } + }, + + clearCache: msg => { + for (let key in cache) { + delete cache[key]; + } + }, + + wasm: data => { + if (data.enable) { + base.wasm_ctrl.enable(); + } else { + base.wasm_ctrl.disable(); + } + }, + + bad: (data, seq, cmd) => { + reply({ seq, error: `invalid command (${cmd})` }); + } +}; + +moto.broker.publish("minion.started", { funcs, cache, reply, log }); + +// Future exports for src/kiri-run/minion.js when unwrapped from gapp.register() + +// Utility functions +export { reply, log }; + +// Constants +export { POLY, clib, ctyp, ptyp, cfil }; + +// Global variables +export { cache, name }; + +// Default export - the main minion functionality +export default funcs; \ No newline at end of file diff --git a/src2/kiri-run/worker.js b/src2/kiri-run/worker.js new file mode 100644 index 00000000..b6156114 --- /dev/null +++ b/src2/kiri-run/worker.js @@ -0,0 +1,750 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +console.log('TOP OF WORKER'); + +import "../ext/clip2.js"; +import "../ext/earcut.js"; +import '../add/array.js'; +import '../add/class.js'; +import '../add/three.js'; + +import { base } from '../geo/base.js'; +import { broker } from '../moto/broker.js'; +import { codec } from '../kiri/codec.js'; +import { util } from '../geo/base.js'; +import { newPoint } from '../geo/point.js'; +import { polygons } from '../geo/polygons.js'; +import { newPrint } from '../kiri/print.js'; +import { render } from '../kiri/render.js'; +import { wasm_ctrl } from '../geo/wasm.js'; +import { version } from '../moto/license.js'; +import { Widget, newWidget } from '../kiri/widget.js'; + +import { CAM } from '../kiri-mode/cam/driver-be.js'; + +// const CAM = {}; +const { time } = util; +const POLY = polygons; + +let debug = (self.debug === true), + drivers = { + CAM + }, + ccvalue = self.navigator ? self.navigator.hardwareConcurrency || 0 : 0, + concurrent = Math.min(4, self.Worker && ccvalue > 3 ? ccvalue - 1 : 0), + current = { + print: null, + snap: null, + mode: null, + }, + wgroup = {}, + wcache = {}, + pcache = {}, + minions = [], + minionq = [], + minifns = {}, + miniseq = 0; + +const worker = self.kiri_worker = { + current, + drivers, + version +}; + +// catch clipper alerts and convert to console messages +self.alert = function(o) { + console.log(o); +}; + +self.uuid = ((Math.random() * Date.now()) | 0).toString(36); + +function minhandler(msg) { + let data = msg.data; + let seq = data.seq; + let fn = minifns[seq]; + if (!fn) { + throw `missing dispatch ${seq}`; + } + delete minifns[seq]; + fn(data); +} + +// for concurrent operations +const minwork = worker.minions = { + get concurrent() { + return concurrent + }, + + get running() { + return minions.length; + }, + + start() { + if (minions.length || !concurrent) { + return; + } + for (let i=0; i < concurrent; i++) { + let minion = new Worker(`/v2/lib/kiri-run/minion.js`); + minion.onmessage = minhandler; + minion.postMessage({ cmd: "label", name: `#${i}` }); + minions.push(minion); + } + console.log(`kiri | init pool | ${version || "rogue"} | ${concurrent + 1}`); + }, + + stop() { + for (let minion of minions) { + minion.terminate(); + } + minions.length = 0; + }, + + union(polys, minarea) { + return new Promise((resolve, reject) => { + if (concurrent < 2 || polys.length < concurrent * 2 || POLY.points(polys) < concurrent * 50) { + resolve(POLY.union(polys, minarea, true)); + return; + } + let polyper = Math.ceil(polys.length / concurrent); + let running = 0; + let union = []; + let receiver = function(data) { + let polys = codec.decode(data.union); + union.appendAll(polys); + if (--running === 0) { + resolve(POLY.union(union, minarea, true)); + } + }; + for (let i=0; i { + if (concurrent < 2) { + resolve(POLY.fillArea(polys, angle, spacing, [], minLen, maxLen)); + return; + } + const state = { zeros: [] }; + minwork.queue({ + cmd: "fill", + polys: codec.encode(polys, state), + angle, spacing, minLen, maxLen + }, data => { + let arr = data.fill; + let fill = []; + for (let i=0; i { + if (concurrent < 2) { + reject("concurrent clip unavailable"); + } + const state = { zeros: [] }; + + minwork.queue({ + cmd: "clip", + polys: codec.encode(POLY.flatten(polys).map(poly => codec.encodePointArray2D(poly.points, state)), state), + lines: codec.encode(lines.map(array => codec.encodePointArray2D(array, state)), state), + z: slice.z + }, data => { + let polys = codec.decode(data.clips); + for (let top of slice.tops) { + for (let poly of polys) { + if (poly.isInside(top.poly)) { + top.fill_sparse.push(poly); + } + } + } + resolve(polys); + }, state.zeros); + }); + }, + + sliceZ(z, points, options) { + return new Promise((resolve, reject) => { + if (concurrent < 2) { + reject("concurrent slice unavaiable"); + } + let { each } = options; + // todo use shared array buffer? + let i = 0, floatP = new Float32Array(points.length * 3); + for (let p of points) { + floatP[i++] = p.x; + floatP[i++] = p.y; + floatP[i++] = p.z; + } + const state = { zeros: [] }; + minwork.queue({ + cmd: "sliceZ", + z, + points: floatP, + options + }, data => { + for (let rec of data.output) { + each(rec); + } + resolve(); + }, state.zeros); + }); + }, + + queue(work, ondone, direct) { + if (direct) { + return ondone(work); + } + let seq = ++miniseq; + minifns[seq] = ondone; + minionq.push({ seq, work }); + minwork.kick(); + }, + + queueAsync(work, direct) { + return new Promise((resolve, reject) => { + minwork.queue(work, resolve, direct); + }); + }, + + kick() { + if (minionq.length === 0 || minions.length === 0) { + return; + } + let minion = minions.shift(); + let { seq, work } = minionq.shift(); + work.seq = seq; + minion.postMessage(work); + minions.push(minion); + }, + + broadcast(cmd, data, direct) { + if (direct) { + return; + } + for (let minion of minions) { + minion.postMessage({ cmd, data }); + } + } +}; + +console.log(`kiri | init work | ${version || "rogue"}`); + +// code is running in the worker / server context +const dispatch = { + pool_start(data, send) { + minwork.start(); + send.done({}); + }, + + pool_stop(data, send) { + minwork.stop(); + send.done({}); + }, + + group: wgroup, + + cache: wcache, + + snap(data, send) { + current.snap = data; + send.done(); + }, + + png(data, send) { + if (current.snap) { + let sws = current.snap.url; + let b64 = atob(sws.substring(sws.indexOf(',') + 1)); + let bin = Uint8Array.from(b64, c => c.charCodeAt(0)); + let img = new png.PNG(); + img.parse(bin, (err, data) => { + send.done({png: bin}, [ bin ]); + }); + } else { + send.done({error: "missing snapshot"}); + } + }, + + // purge all sync data + clear(data, send) { + // current.snap = null; + current.print = null; + dispatch.group = wgroup = {}; + dispatch.cache = wcache = {}; + Widget.Groups.clear(); + send.done({ clear: true }); + }, + + // widget sync + sync(data, send) { + // ensure each widget belongs to at least one group + let group = wgroup[data.group]; + if (!group) { + group = []; + group.id = data.group; + wgroup[data.group] = group; + } + + let vertices = data.vertices, + widget = newWidget(data.id, group) + .setInWorker() + .loadVertices(vertices); + + // do it here so cancel can work + wcache[data.id] = widget; + // stored for possible future rotations + widget.vertices = vertices; + // restore meta + widget.meta = data.meta; + // restore tracking object + widget.track = data.track; + send.done(data.id); + }, + + // belt mode rotate widgets 45 degrees on X axis before slicing + rotate(data, send) { + let { settings } = data; + if (!settings.device.bedBelt) { + return send.done({}); + } + + function mins(vert, last = {}) { + let miny = last.miny || Infinity, + maxy = last.maxy || -Infinity; + for (let i=0, l=vert.length; i { + now = time(); + // on alert + if (alert) send.data({ alert }); + // on update + if (now - last < 10 && update < 0.99) return; + if (update || msg) send.data({update: (0.05 + update * 0.95), updateStatus: msg}); + last = now; + }, (error) => { + if (error) { + send.data({error: error}); + } else { + const slices = widget.slices || []; + send.data({send_start: time()}); + send.data({ + stats: widget.stats, + slices: slices.length, + }); + slices.forEach((slice,index) => { + const state = { zeros: [] }; + send.data({index: index, slice: slice.encode(state)}, state.zeros); + }) + send.data({send_end: time()}); + } + send.done({done: true}); + widget.points = undefined; + }); + }, + + sliceAll(data, send) { + const { settings } = data; + const { mode } = settings; + const driver = drivers[mode]; + + if (driver.sliceAll) { + driver.sliceAll(settings, send.data); + } + + send.done({done: true}); + }, + + prepare(data, send) { + // create widget array from id:widget cache + const widgets = Object.values(wcache); + + // let client know we've started + send.data({update:0.05, updateStatus:"preview"}); + + const { settings } = data; + const { mode } = settings; + const driver = drivers[mode]; + + if (!(driver && driver.prepare)) { + return console.log({ invalid_print_driver: mode, driver }); + } + + driver.prepare(widgets, settings, (progress, message, layer) => { + const state = { zeros: [] }; + const emit = { progress, message, layer: (layer ? layer.encode(state) : undefined) }; + send.data(emit, state.zeros); + }).then(() => { + const unitScale = settings.controller.units === 'in' ? (1 / 25.4) : 1; + const print = current.print || {}; + const minSpeed = (print.minSpeed || 0) * unitScale; + const maxSpeed = (print.maxSpeed || 0) * unitScale; + + send.data({ progress: 1, message: "transfer" }); + send.done({ done: true, minSpeed, maxSpeed }); + }); + }, + + export(data, send) { + const mode = data.settings.mode; + const driver = drivers[mode]; + + if (!(driver && driver.export)) { + console.log({missing_export_driver: mode}); + return send.done() + } + + let output; + driver.export(current.print, (line, direct) => { + send.data({line}, direct); + }, (done) => { + // SLA workaround + output = done; + }, (debug) => { + send.data({debug}); + }); + + //export in different modes, based on device type + if(mode.toUpperCase() === "LASER"){ + let FExt = data.settings.device.gcodeFExt.toUpperCase(); + if(FExt === "DXF"){ + let dxf = driver.exportDXF(data.settings, current.print.output); + send.data({line:dxf}); + }else if(FExt === "SVG"){ + let svg = driver.exportSVG(data.settings, current.print.output); + send.data({line:svg}); + }else{ + //if not svg or dxf, default to gcode + let gcode = driver.exportGCode(data.settings, current.print.output); + send.data({line:gcode}); + } + } + + const { + bounds, + time, + lines, + bytes, + distance, + settings, + segments, + purges, + labels + } = current.print; + + send.done({ + done: true, + output: output ? output : { + bounds, + time, + lines, + bytes, + distance, + settings, + segments, + purges, + labels + } + }); + }, + + colors(data, send) { + const { colors, max } = data; + const colorMap = {}; + colors.forEach(color => { + colorMap[color] = render.rate_to_color(color, max); + }); + send.done(colorMap); + }, + + parse(args, send) { + const { settings, code, type } = args; + const origin = settings.origin; + const offset = { + x: origin.x, + y: -origin.y, + z: origin.z + }; + const device = settings.device; + const print = current.print = newPrint(settings, Object.values(wcache)); + const tools = device.extruders; + const mode = settings.mode; + const thin = settings.controller.lineType === 'line' || mode !== 'FDM'; + const flat = settings.controller.lineType === 'flat' && mode === 'FDM'; + const parsed = print.parseGCode(code, offset, progress => { + send.data({ progress: progress * 0.25 }); + }, done => { + const minSpeed = print.minSpeed; + const maxSpeed = print.maxSpeed; + render.path(done.output, progress => { + send.data({ progress: 0.25 + progress * 0.75 }); + }, { thin: thin || print.belt, flat, tools }) + .then(layers => { + send.done({parsed: codec.encode(layers), maxSpeed, minSpeed}); + }); + }, { + fdm: mode === 'FDM', + cam: mode === 'CAM', + belt: device.bedBelt + }); + }, + + parse_svg(parsed, send) { + parsed.forEach(layer => { + layer.forEach(out => { + const { x, y, z } = out.point; + out.point = newPoint(x,y,z || 0); + }); + }); + const print = current.print = newPrint(null, Object.values(wcache)); + render.path(parsed, progress => { + send.data({ progress }); + }, { thin: true }) + .then(layers => { + send.done({parsed: codec.encode(layers)}); + }); + }, + + config(data, send) { + const update = {}; + if (data.base) { + update.base = data.base; + Object.assign(base.config, data.base); + } else { + console.log({invalid:data}); + } + for (let minion of minions) { + minion.postMessage({ + cmd: "config", + base: data.base + }); + } + send.done({config: update}); + }, + + image2mesh(info, send) { + let { device } = info.settings; + load.PNG.parse(info.png, { + outWidth: device.bedDepth, + outHeight: device.bedWidth, + inv_image: info.inv_image, + inv_alpha: info.inv_alpha, + border: info.border, + blur: info.blur, + base: info.base, + progress(progress) { send.data({ progress }) }, + done(vertices) { send.done({ vertices }, [ vertices.buffer ])} + }); + }, + + gerber2mesh(data, send) { + const vertices = load.GBR.toMesh(data, { progress(pct) { + send.data({ progress: pct/100 }); + } } ); + send.data({ vertices }, [ vertices.buffer ]); + }, + + zip(data, send) { + let { files } = data; + let zip = new JSZip(); + for (let file of files) { + zip.file(file.name, file.data); + } + zip.generateAsync({ + type: "uint8array", + compression: "DEFLATE", + compressionOptions: { level: 6 }, + streamFiles: true + }, progress => { + send.data(progress); + }).then(output => { + send.done(output); + }); + }, + + wasm(data, send) { + if (data.enable) { + wasm_ctrl.enable(); + } else { + wasm_ctrl.disable(); + } + minwork.broadcast("wasm", { enable: data.enable ? true : false }); + send.done({ wasm: data }); + }, + + putCache(msg, send) { + const { key, data } = msg; + // console.log({ worker_putCache: key, data }); + if (data) { + pcache[key] = data; + } else { + delete pcache[key]; + } + minwork.broadcast("putCache", msg); + send.done({ ok: true }); + }, + + clearCache(msg, send) { + pcache = {}; + minwork.broadcast("clearCache", msg); + send.done({ ok: true }); + } +}; + +function is_async(fn) { + return fn.constructor.name === "AsyncFunction"; +} + +dispatch.send = (msg, direct) => { + self.postMessage(msg, direct); +}; + +dispatch.onmessage = self.onmessage = async function(e) { + let time_recv = time(), + msg = e.data || {}, + run = dispatch[msg.task], + send = { + data : function(data,direct) { + // if (direct && direct.length) { + // console.log( direct.map(z => z.byteLength).reduce((a,v) => a+v) ); + // } + dispatch.send({ + seq: msg.seq, + task: msg.task, + done: false, + data: data + }, direct); + // if (direct && direct.length) { + // console.log( direct.map(z => z.byteLength).reduce((a,v) => a+v) ); + // } + }, + done : function(data,direct) { + dispatch.send({ + seq: msg.seq, + task: msg.task, + done: true, + data: data + }, direct); + } + }; + + if (run) { + try { + let time_xfer = (time_recv - msg.time), + output = is_async(run) ? await run(msg.data, send) : run(msg.data, send), + time_send = time(), + time_proc = time_send - time_recv; + + if (output) dispatch.send({ + seq: msg.seq, + task: msg.task, + time_send: time_xfer, + time_proc: time_proc, + // replaced on reply side + time_recv: time(), + data: output + }); + } catch (wrkerr) { + console.trace(wrkerr.stack); + send.done({error: wrkerr.toString()}); + } + } else { + console.log({worker_unhandled: e, msg, fn: dispatch[msg.task]}); + } +}; + +// initilize driver mode handlers +CAM.init(dispatch); + +broker.publish("worker.started", { dispatch, minions: minwork }); diff --git a/src2/main/kiri.js b/src2/main/kiri.js new file mode 100644 index 00000000..a3f879dd --- /dev/null +++ b/src2/main/kiri.js @@ -0,0 +1,22 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import '../add/array.js'; +import '../add/class.js'; +import '../add/three.js'; +import '../kiri/lang.js'; +import '../kiri/lang-en.js'; +import { run } from '../kiri/init.js'; + +console.log('KIRI V2'); + +function checkReady() { + if (document.readyState === 'complete') { + console.trace('READY!'); + run(); + } +} + +// when dom + scripts complete +document.onreadystatechange = checkReady; + +checkReady(); \ No newline at end of file diff --git a/src2/mesh/work.js b/src2/mesh/work.js index f29d5e92..dd9f04de 100644 --- a/src2/mesh/work.js +++ b/src2/mesh/work.js @@ -1,8 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -// Import Clipper library first to ensure it's available in worker context import "../ext/clip2.js"; - import "../ext/earcut.js"; import '../add/array.js'; import '../add/class.js'; @@ -18,7 +16,6 @@ import { polygons } from '../geo/polygons.js'; import { CSG } from '../geo/csg.js'; import { util } from './util.js'; import { tool as meshTool } from './tool.js'; -import { THREE } from '../ext/three.js'; const { Triangle, Vector3, BufferGeometry, BufferAttribute, computeFaceNormal } = THREE; diff --git a/src2/moto/license.js b/src2/moto/license.js index 0f0f3b41..f3676c07 100644 --- a/src2/moto/license.js +++ b/src2/moto/license.js @@ -6,5 +6,6 @@ const terms = { VERSION: (is_self ? self : this).debug_version || "4.1.9" }; +export const beta = 0; export const license = terms; export const version = terms.VERSION; diff --git a/src2/webpack/esbuild.config.mjs b/src2/webpack/esbuild.config.mjs index 88f1cb9e..d588d8c7 100644 --- a/src2/webpack/esbuild.config.mjs +++ b/src2/webpack/esbuild.config.mjs @@ -8,13 +8,19 @@ const isProd = mode === 'prod'; console.log(`Building in ${mode} mode...`); -const MESH_OUTFILE = 'src2/pack/mesh.js'; +const MESH_OUTFILE = 'src2/pack/mesh-main.js'; const MESH_EXTRAS = [ 'mod/*/mesh.js', 'mod/*/tra1.js' ]; -async function appendExtraModules(extras, minify = false) { +const KIRI_OUTFILE = 'src2/pack/kiri-main.js'; +const KIRI_EXTRAS = [ + 'mod/*/kiri.js', + 'mod/*/tra1.js' +]; + +async function appendExtraModules(extras, outfile, minify = false) { const files = await glob(extras); if (files.length === 0) return; @@ -32,13 +38,13 @@ async function appendExtraModules(extras, minify = false) { }) ); - await fs.appendFile(MESH_OUTFILE, transformed.join('')); - console.log(`Appended ${files.length} module(s) to ${MESH_OUTFILE}`); + await fs.appendFile(outfile, transformed.join('')); + console.log(`Appended ${files.length} module(s) to ${outfile}`); } async function buildApp() { try { - // Bundle main app + // Bundle mesh main app await build({ entryPoints: [ 'src2/main/mesh.js' ], bundle: true, @@ -54,13 +60,47 @@ async function buildApp() { } }); - appendExtraModules(MESH_EXTRAS, isProd); + appendExtraModules(MESH_EXTRAS, MESH_OUTFILE, isProd); - // Bundle worker + // Bundle mesh worker await build({ entryPoints: [ 'src2/mesh/work.js' ], bundle: true, - outfile: 'src2/pack/work.js', + outfile: 'src2/pack/mesh-work.js', + format: 'esm', + external: ['module'], + sourcemap: !isProd, // true for dev, false for prod + minify: isProd, // false for dev, true for prod + target: 'es2020', + platform: 'browser', + define: { + 'process.env.NODE_ENV': `"${mode}"` + } + }); + + // Bundle mesh main app + await build({ + entryPoints: [ 'src2/main/kiri.js' ], + bundle: true, + outfile: KIRI_OUTFILE, + format: 'esm', + external: ['module'], + sourcemap: !isProd, // true for dev, false for prod + minify: isProd, // false for dev, true for prod + target: 'es2020', + platform: 'browser', + define: { + 'process.env.NODE_ENV': `"${mode}"` + } + }); + + appendExtraModules(KIRI_EXTRAS, KIRI_OUTFILE, isProd); + + // Bundle mesh worker + await build({ + entryPoints: [ 'src2/kiri-run/worker.js' ], + bundle: true, + outfile: 'src2/pack/kiri-work.js', format: 'esm', external: ['module'], sourcemap: !isProd, // true for dev, false for prod diff --git a/web2/kiri/cxdlp.html b/web2/kiri/cxdlp.html new file mode 100644 index 00000000..e77002af --- /dev/null +++ b/web2/kiri/cxdlp.html @@ -0,0 +1,146 @@ + + + + + + + + +
+ + + +
+ +
+
+ + + +
+ + diff --git a/web2/kiri/engine.html b/web2/kiri/engine.html new file mode 100644 index 00000000..9d8eb8c6 --- /dev/null +++ b/web2/kiri/engine.html @@ -0,0 +1,369 @@ + + + Kiri:Moto Javascript Engine API + + + + + + + + + + + + + + + + + +

Kiri:Moto Javascript Engine API

+
+
+
JavaScript
+
+
+
+
+
Output
+
+
+
+
+
+
+

Load Examples

+
+ + + +
+
+
+

Engine Methods

+
  • - where URL points to an STL
  • +
  • - where data is a string or binary STL
  • +
  • - FDM, CAM, LASER, SLA (untested)
  • +
  • - function to receive engine progress messages
  • +
  • - change default object parameters
  • +
  • - change default process parameters
  • +
  • - set stock dimensions for CAM mode
  • +
  • - set available tools for CAM operations
  • +
  • - absolute move origin of loaded object
  • +
  • - relative move origin of loaded object
  • +
  • - scale axes of loaded object
  • +
  • - rotate axes of loaded object in radians
  • +
  • - async slice loaded object
  • +
  • - async path routing of slice data
  • +
  • - async gcode generation from path routing
  • +
    + + + diff --git a/web2/kiri/favicon-mobile.png b/web2/kiri/favicon-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..f8acc4c9b9c13df4976d2b37bc88f228c62873a6 GIT binary patch literal 6142 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 literal 0 HcmV?d00001 diff --git a/web2/kiri/frame.html b/web2/kiri/frame.html new file mode 100644 index 00000000..b89ae43e --- /dev/null +++ b/web2/kiri/frame.html @@ -0,0 +1,164 @@ + + + Kiri:Moto Frame Message API + + + + + + + + + + + + + + + + + +

    Kiri:Moto Frame Message API

    +
    +
    +
    code
    +
    +
    +
    output
    +
    +
    +
    +
    frame
    + +
    +
    +
    +

    Frame Message API

    +
  • - where URL points to an STL
  • +
  • - clear workspace
  • +
  • - data is string or binary. type is "stl" or "svg"
  • +
  • - id or object of iframe with Kiri:Moto loaded
  • +
  • - FDM, CAM, LASER, SLA
  • +
  • - change default object parameters
  • +
  • - change default process parameters
  • +
  • - async slice loaded object. event upon completion
  • +
  • - async path routing of slice data. event upon completion
  • +
  • - async gcode generation. opens export dialog or event callback with gcode
  • +
  • - function to receive all messages
  • +
  • - function to receive named messages
  • +
  • - send named event with data
  • +
  • - show alert message with optional time in seconds
  • +
  • - set progress bar (0-1) with optional message
  • +
    + + diff --git a/web2/kiri/gsbox.png b/web2/kiri/gsbox.png new file mode 100644 index 0000000000000000000000000000000000000000..2985505dac0f6ba70592a598c3fd8ae6860650ed GIT binary patch literal 65184 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%0DXc_$(>vh=)tC61T5~ z8=KVC*WJx+=7Bg66Y0k0Dlvml1Jn4zYI0f1VTsuTI4y?cyogr(lTh9mCm!nSqrmoY z8cUGUkGonF=maR{6R-rNl*5v924KoXsljJtu`YlG9Teohg<@i2>=eut_CJ>%OLwDu z_|p@~mOsiwY=0~R3V04{89c;T7clZ^2C*|y4EhJLZP(xOrw$!kNF;8DU-ovcNd&9H zjWKGC&Cy#$xec(8!vMu4h3nx7W@x5>m&G;j^1!9w6BcT~MWAvV73vK+Dk{|@u8LqK z3U%A_k9sN?e4;RTQ-JUHsmw_2Xz#nPj2?UYJCphRKhRL<)x}JK3U$)NAIpRS9XiZzX2Zdlb?4^E1E0TwaXMbl2MM*vJ@H3Jpl zcvz~XV}J$a7!C(gj0^K+q2^1%?sZ&Myq-AsB-f%o>ZIIWiYfw^2aixK>Y1mW?l2$F$%}^`X(p)Uw>kJI{2;j)4P3H z32(dytd;PTl^qK8Tk!vIPy4o8px^4+f&KpoKA{^M1+LgyMDcp6%YJ0D3;-3;$?3U7 z?`8TeM@oX1R!LAIMggcoU{a1P4p!7jQ8WxeO5zX$MHHk>)ttF~Db`J-2cf4r_}7_{ z!AG!2dDQBRgR*Oe50@bYyhykVAL>QAL@`nHTZN#%Z!$Cdm$|`%Lksl{cS1~Tg{cD^ zK~YY`G9Lm=`~d0@g9tStmt@x{HVG>6qnBTToYAK#)M@xB z#-FsO!g3m#DL_w^pB%4E_Z{d-AAaNB-1Nlrb3rhT(rBi@>dG379DgiJ3V04{Sw7aw zb_yH`&_dnVw5(mb_Hnqe`FZQQYhbih$4yKF*l|Hee-)T1T`aAhAtEu6;-8yoB$q|5 zi(n*Tp|Ds?Q@j)Sk_M?#op}T+1v83)YMwxj3ZPY_*#gDC^VCz-&xzZY6a5oM{`X5v z`~6FMwiE5|AxL?kQi80QZx2{2=22g;qvTM;45A?ju1j6B{aNf*{Vi-&PcVXhi~C^I z1FN_VnTd(wW1hSFsF+Bg3b%z1MMDKCoDpCmI5|C)#D%*8aB)_UyD0;}C=L>+0A35{ zQD6~nU$Vpf=W-vu^_%f*eh=CSrm7pr*v)s=E7ULeIG2B>2dw4)j4#(iyaUlPAMEDy z%_NrjBs;FXp`odTx+*U7!RCpaT;%c|09TzZ^%4D*0he-I`Z_tLN|-M5RmDaKXc4Rk zRMbyN9Q*yz?F*LE;BHJ57V4)nBSCiP&_5?n_dh%n1gES6UQczoC%FR7d%#)&5BXzu z5&Fz{JaDBuCh zzXs(L2ss&Sim|qFL&uY?9XI}%yH!K4Re+F3OZfz66P4pK3wb9ZmX#*xBIC>g^Pq7LZGP%!@vYa-hh9%3Tq#gx50AQU!Z~i?S&OCbGi= z#fhQ)duz{To`lsWEEQ;$3Wb?pP6HD7qnrXBu*xao({Tzw&fAHP?pB=)g739-?Rchb z!?tY=bYr9I6S;Y!8ky3V6nCot$OtF#C>UZoPdzI@)2?n`^0_p;DI9qz|KYLS5dV(h z5+30sfF}L7r$8wMJYbd5!>2Ax0dPFsz@2U)P$HGAeJHupdS6qotbLLUWhr?m-qw4nkXoE3Cd2;nKAwhlhkgWSl#_|&O4 zAIs!}KWeeEoJJThP5W<8flE`s1JDlD^t#{MgDQ5fT zVHjG6dBP%R5MfH1m`tAzCe9rC{n@dRZrq!(BIDp^Sw8R-xHJVkU|pI%{_MpmP@>;z z3WEFEuD#{y^=CW + + + + + + + + + + + + gyroid testing + + + + +
    +
    + +
    + offset + raw + caps + fast +
    +
    + + + +
    +
    + + diff --git a/web2/kiri/index.css b/web2/kiri/index.css new file mode 100644 index 00000000..a35f0b7f --- /dev/null +++ b/web2/kiri/index.css @@ -0,0 +1,2563 @@ +:root { + --fg-gray: #495057; + --bg-gray: rgba(0,0,0,0.25); + --bg-gray-lt: rgba(0,0,0,0.15); + --gradbar: linear-gradient(to right, #ddd, #666, #ddd); + --gradbar2: linear-gradient(to right, #ddd, #999, #ddd); + --gradbar-vert: linear-gradient(to bottom, #ddd, #666, #ddd); + --holdbar: linear-gradient(to top, rgba(100,100,100,0.1), rgba(255,255,255,1), rgba(100,100,100,0.1)); + --holdbar-dk: linear-gradient(to top, rgba(100,100,100,0.1), rgba(255,255,255,0.25), rgba(100,100,100,0.1)); + --holdbar-sf: linear-gradient(to top, rgba(200,200,200,0.1), rgba(255,255,255,1), rgba(200,200,200,0.1)) !important; + --highlight: rgba(126,153,183,0.4); + --main-color: rgba(126,153,183,1); + --main-color-hover: rgb(96,127,164); + --row-hover: rgb(126,153,183,0.25); + --work-area: transparent; + --header: rgba(126,153,183,0.5); + --menu: #f0f0f0; + --menu-text: #222; + --menu-border: #555; + --dark-menu-text: #fff; + --menu-hover: #ddd; + --blue-00: rgb(31, 48, 66); + --blue-0: rgb(48, 64, 82); + --blue-1: rgb(96,127,164); + --blue-2: rgb(126,153,183); + --blue-3: rgb(190, 204, 219); + --blue-4: rgb(221, 228, 234); + --blue-5: rgb(240, 243, 245); + --blue-6: rgb(247, 249, 250); + --slide-bg: rgba(190, 204, 219, 0.5); + --scroll-bg: transparent; +} + +/* surface size configuration and font import */ +@font-face { + font-family: 'Russo One'; + src: url('/moto/russo-one.ttf'); +} +@media only screen and (max-width : 1000px) { + .top-menu > span > label { + display: none; + } +} +@media only screen and (max-width : 1200px) { + body { + font-size: smaller; + } +} +@media only screen and (max-width : 900px) { + body { + font-size: x-small; + } +} +@media only screen and (max-height : 1024px) { + body { + font-size: smaller; + } +} +@media only screen and (max-height : 750px) { + .lt-menu, .pop-lcol { + font-size: 16px !important; + } + .lt-menu svg { + font-size: 30px !important; + } + .pop-lcol svg { + font-size: 20px !important; + } + .mod-x { + position: absolute; + font-size: 30px; + top: 8px; + } +} + +html, body { + position: relative; + height: 100%; + margin: 0; + scrollbar-width: none !important; + scrollbar-color: var(--blue-2) var(--scroll-bg); +} +/* default element configuration */ +a, a:hover, a:visited { + border: none; + color: inherit; + text-decoration: none; +} +div { + position: relative; +} +button.yellow { + background-color: #eda; + border: 1px solid rgba(0,0,255,0.5); +} +button.yellow:hover { + background-color: #eea; + +} +button { + display: flex; + border: 1px solid #ccc; + border-radius: 3px; + background-color: #eee; + padding: 3px 7px 3px 7px; + margin: 1px 1px 2px; + cursor: pointer; + text-transform: capitalize; + white-space: nowrap; +} +button:not([disabled]):hover { + background-color: var(--main-color); + color: white; +} +button[load] { + width: 100%; + text-align: left; +} +button[del] { + margin-left: 5px; +} +button[disabled] { + color: #999; +} +input { + background-color: #f8f8ff; + margin-bottom: 1px; + text-align: right; + border: 0.5px solid #bbb; + padding-top: 0; + padding-bottom: 0; + margin-top: 1px; + position: relative; +} +input:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgb(0 100 255 / 10%); +} +input[disabled] { + background-color: #ddd; + color: #000; +} +input[type="range"] { + width: 85px; + background-color: transparent; +} +input[type=range]::-moz-focus-outer { + border: 0; +} +input[type="checkbox"] + .color { + z-index: 10000; + width: 15px; + height: 15px; + border: 1px solid rgba(0,0,0,0.1); + border-radius: 3px; + display: inline-block; + pointer-events: none; + margin: 2px; +} +input[type="checkbox"]:not(:checked) + .color { + border: 1px solid blue; + background-color: rgba(255,255,255,0.5) !important; +} +input[type="checkbox"].color { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + width: 100%; + height: 100%; +} +select, textarea, button, input, a { + outline: none; +} +textarea { + overflow: scroll; +} +select { + border: 1px solid #bbb; + background-color: rgba(255,255,255,0.5); + border-radius: 3px; +} +th, tr, td, span, div, label, button { + user-select: none; +} +th, tr, td, label { + white-space: nowrap; +} +details summary { + list-style: none; +} +details summary::-webkit-details-marker { + display: none; +} +details summary::before { + content: ""; +} +details summary { + position: relative; + cursor: pointer; +} +details summary::after { + content: "▸"; + position: absolute; + right: 5px; + font-size: 1em; + transform: rotate(-90deg); + transition: transform 0.2s ease; +} +details[open] summary::after { + transform: rotate(90deg); +} + +/* for bambu mgr dialog */ +.video { + border: 1px solid #888; + border-radius: 3px; +} + +/* container for entire page / app */ +#app { + position: fixed; + font-weight: normal; + font-family: sans-serif; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +#app-name .km-font { + font-size: larger; +} + +/** dark conversion */ +.dark button[disabled] { + color: #888; +} +.dark input[disabled] { + border-color: #666; + background-color: #444; + color: #888; +} +.dark input, +.dark select, +.dark option +{ + border-color: #888; + background-color: #333; + color: #fff; +} +.dark hr { + border-color: #555; +} +.dark textarea { + background-color: #333; + color: #fff; +} +.dark button { + color: #fff; + background-color: #555; + border: 1px solid #777; +} +.dark button:not([disabled]):hover { + background-color: var(--blue-2); + color: black; +} +.dark #progress { + background-color: #333; + border-top: 1px solid #555; + border-bottom: 1px solid #555; +} +.dark #mesh-info label, +.dark #mode-info label +{ + color: #fff; + border-color: #888; + background-color: var(--blue-0); +} +.dark #mesh-info span, +.dark #mode-info span +{ + color: #fff; + border-color: #888; + background-color: #333; +} +.dark #oplist div .del { + color: #aaa; +} +.dark #oplist div:hover .del { + color: #333 !important; +} +.dark .mod-top, .dark .mod-end { + background-color: var(--blue-00); +} +.dark .pop-sep, +.dark .set-sep +{ + background-color: #555 !important; +} +.dark #render-tools .pop, +.dark #render-tools svg, +.dark #lang-pop, +.dark .pop-tics > div, +.dark #top, +.dark #top .content, +.dark #app-name, +.dark .set2-group, +.dark .set2-group .var-row, +.dark .set2-sep, +.dark .var-row, +.dark #layers .var-row, +.dark #slider-zero, +.dark #slider-max, +.dark .mdialog, +.dark .t-body, +.dark .cam-pop-op, +.dark .mod-print, +.dark .mod-print .header label +{ + background-color: #282828; + color: white; +} +.dark #dev-list:hover { + background-color: var(--blue-1); +} +.dark #render-tools .pop, +.dark #render-tools svg, +.dark .cam-pop-op { + border-color: #555; +} +.dark .btn-sel, +.dark .txt-sel button { + /* color: #fff; */ + background-color: var(--blue-1); +} +.dark #dialog { + background-color: rgba(60,60,60,0.85); + border-top: 8px solid rgba(100,100,100,1); + border-bottom: 7px solid rgba(100,100,100,1); +} +.dark #modal, .dark #dialog { + color: #fff !important; +} +.dark #modal select, .dark #dialog select { + color: #fff !important; +} +.dark .t-body, +.dark .set2-group { + border: 1px solid #444 !important; +} +.dark .set2-group .var-row:hover, +.dark .var-row:hover +{ + border-right: 3px solid var(--blue-3); + background-color: var(--blue-1) !important; +} +.dark #slider-line { + background-color: #444; +} +.dark .set-header { + background-color: var(--blue-00); +} +.dark #oplist > div, +.dark .opdiv +{ + background-color: #333; + border: 1px solid #888; +} +.dark #lang-pop > div:hover, +.dark #oplist > div:hover, +.dark #op-add:hover { + background-color: var(--blue-1); +} +.dark #oplist div:hover .del { + color: #777; +} +.dark #top .content > div:hover { + background-color: var(--blue-1); +} +.dark .top-menu label { + color: var(--dark-menu-text) +} +.dark #render-tools svg:hover { + background-color: var(--blue-1); + border-color: var(--blue-3); +} +.dark .top-menu > span:hover { + background-color: var(--blue-1); +} +.dark .top-menu > span:hover label, +.dark .top-menu > span:hover svg +{ + color: var(--dark-menu-text) !important; +} +.dark .top-menu > span.selected, +.dark .top-menu > span.selected svg, +.dark .top-menu-drop div.selected +{ + background-color: var(--blue-1); + color: #fff; +} +.dark #slider-hold { + background-color: var(--blue-0); +} +.dark #stats { + color: #888; +} +.dark #rnfo { + color: #aaa; +} +.dark #rnfo label { + color: #ada; +} +.dark #slider-center { + /* background-color: rgba(255,255,255,0.15); */ +} +#slider-center { + /* background-color: rgba(255,255,255,0.5); */ +} +.dark #slider-hold .handle { + background-color: #999; +} +.dark #speedbar label { + color: black; +} +.dark .widopt { + background-color: #222; +} + +/* top menu bar, drop menus */ +#top, #app-name { + background-color: #fff; + text-transform: capitalize; + font-family: sans-serif; + z-index: 15; + height: 36px; +} +.top-menu { + display: flex; + gap: 0px; + align-items: center; + white-space: nowrap; + align-items: stretch; +} +.top-menu > span { + padding: 5px 9px 5px 9px; + border-left: 1px solid transparent; + border-right: 1px solid transparent; +} +.top-menu hr { + width: calc(100% - 3px); + margin: 0; + border: 0; + padding: 0; + height: 0.5px; + background-color: var(--menu-border); +} +.top-menu label { + color: #444; +} +.top-menu svg { + color: var(--blue-1); +} +.top-menu > span { + position: relative; + display: flex; + gap: 5px; + align-items: center; +} +.top-menu > span:hover label { + color: #000; +} +.top-menu > span:hover > svg { + color: black; +} +.top-menu > span:hover { + background-color: var(--blue-4); + border-left: 1px solid var(--blue-2); + border-right: 1px solid var(--blue-2); +} +.top-menu > span.selected, +.top-menu-drop div.selected +{ + background-color: var(--blue-3); +} +.top-menu > span:hover .top-menu-drop { + display: flex; +} +.top-menu-left .content { + border-top-right-radius: 4px; +} +.top-menu-right { + align-items: self-end; +} +.top-menu-center .content > div > label { + text-align: center !important; +} +.top-menu-right .content { + border-top-left-radius: 4px; +} +.top-menu-drop { + position: absolute; + display: none; + top: 100%; + left: -1px; + right: -1px; + flex-direction: column; + font-family: sans-serif; +} +.top-menu-drop svg { + width: 15px; +} + +#top .content { + gap: 2px; + display: flex; + align-items: center; + flex-direction: column; + background-color: #fff; + border-top: 1px dashed var(--blue-2); + border-left: 1px solid var(--blue-2); + border-right: 1px solid var(--blue-2); + border-bottom: 1px solid var(--blue-2); + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +#top .content { + display: flex; + flex-direction: column; + align-items: stretch; + padding: 2px; +} +#top .content > div { + display: flex; + flex-direction: row; +} +#top .content > div > label { + text-align: left; + flex-grow: 1; +} +#top .content > div > span { + text-align: right; +} +#top .content > div > span { + padding: 2px 7px 2px 7px; + padding-left: 10px; + text-align: center; +} +#top .content > div > label { + padding: 2px 7px 2px 7px; + justify-self: stretch; + align-self: stretch; + border-radius: 3px; +} +#top .content > div:hover { + background-color: var(--blue-4); +} + +#app-name { + position: absolute; + top: 0; + left: 50%; + z-index: 50; + transform: translateX(-50%); +} +#app-name { + cursor: pointer; + +} +#app-name hr { + margin: 1px; +} +#app-name:hover #app-name-pop { + display: flex; +} +#app-name-pop { + color: var(--fg-gray); + font-family: sans-serif; + position: absolute; + top: 100%; +} +#app-name-pop .content { + text-align: center; +} +#app-lang { + justify-content: flex-end; +} +#app-lang:hover #lang-pop { + display: flex; +} + +#app,div { + display: flex; + padding: 0; + border: 0; + margin: 0; +} +#loading { + width: 40px; + height: 40px; + padding-right: 15px; + position: fixed; + display: none; +} + +.hide-scroll { + scrollbar-color: transparent transparent !important; +} +.nooutline { + outline: none; +} +.noselect { + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.km-font { + font-family: 'Russo One', sans-serif; +} +.menu-title { + font-size: larger; + font-weight: bold; +} +.b0 { + border: 0px; +} +.bt1 { + border: 1px solid transparent; +} +.bt2 { + border: 2px solid transparent; +} +.bred { + border-color: red !important; +} +.checker { + background: + linear-gradient(135deg, transparent 45%, #555 45%, #555 55%, transparent 55%) !important; + background-size: 10px 10px; + background-position: 0 0, 5px 5px; +} +.contents { + display: contents; +} +.full { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} +.nope, .nope * { + pointer-events: none; +} +.allpe { + pointer-events: auto !important; +} +.hide { + display: none !important; +} +.noshow { + display: none; +} +.mono { + font-family: monospace; +} +.font-smol { + font-size: smaller; +} +.font-tiny { + font-size: x-small; +} +.gap1 { + gap: 1px; +} +.gap2 { + gap: 2px; +} +.gap3 { + gap: 3px; +} +.gap4 { + gap: 4px; +} +.gap5 { + gap: 5px; +} +.gap10 { + gap: 10px; +} +.pad3 { + padding: 3px !important; +} +.pad4 { + padding: 4px !important; +} +.pad5 { + padding: 5px !important; +} +.h100 { + height: 100%; +} +.grid { + display: grid; +} +.frow, .f-row { + flex-direction: row; +} +.fcol, .f-col { + flex-direction: column; +} +.f-grow > * { + flex: 1; +} +.auto { + flex-basis: auto; +} +.basis-50 { + flex-basis: 50%; +} +.grow0 { + flex-grow: 0 !important; +} +.grow { + flex-grow: 1; +} +.shrink0 { + flex-shrink: 0; +} +.a-start { + align-items: flex-start; +} +.a-center { + align-items: center; +} +.a-end { + align-items: flex-end; +} +.a-stretch { + align-items: stretch; +} +.j-start { + justify-content: flex-start; +} +.j-center { + justify-content: center; +} +.j-end { + justify-content: flex-end; +} +.j-stretch { + justify-items: stretch; +} +.as-stretch { + align-self: stretch; +} +.space-around { + justify-content: space-around; +} +.space-between { + justify-content: space-between; +} +.center, .t-center { + text-align: center; +} +.t-left { + text-align: left; +} +.t-just { + text-align: justify; +} +.t-right { + text-align: right; +} +.russo { + font-family: 'Russo One', sans-serif; +} +.overy { + overflow-y: auto; +} +.mh0 { + min-height: 0; +} +.mh35 { + max-height: 35%; +} +.mh85 { + max-height: 85%; +} +.mlr10 { + margin: 0 10px 0 10px; +} +.mt10 { + margin-top: 5px; +} + +#curtain { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: fixed; + color: black; + background-color: #fff; + font-family: 'Russo One', sans-serif; + z-index: 1000; +} + +#tracker { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + display: none; + z-index: 101; +} + +#container { + border: 0; + margin: 0; + padding: 0; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; +} +#container canvas { + overflow: hidden; + margin: 0; + border: 0; + padding: 0; + width: 100%; + height: 100%; +} + +.chonk { + min-height: 3px; + max-height: 3px; + min-width: 100px; + background-color: var(--bg-gray-lt); +} +.chonk2 { + min-height: 3px; + max-height: 3px; + min-width: 100px; + background-color: var(--bg-gray-lt); +} +#mode-info { + white-space: nowrap; + font-size: smaller; + font-family: sans-serif; + align-items: stretch; + justify-content: center; + padding: 2px 0 2px 0; + display: grid !important; + margin-right: 0 !important; + margin-bottom: 2px !important; + grid-template-columns: auto 1fr; + row-gap: 1px; +} +#mode-info > div { + display: contents; +} +#mode-info label, #mesh-info label { + font-size: smaller; + font-weight: bold; + align-self: right; + margin: 0 0 0 5px; + padding: 3px 5px 3px 5px; + color: var(--fg-gray); + background-color: var(--blue-4); + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + border-top: 1px solid var(--blue-3); + border-left: 1px solid var(--blue-3); + border-right: 0; + border-bottom: 1px solid var(--blue-3); +} +#mode-info span, #mesh-info span { + font-size: smaller; + margin: 0 5px 0 0; + padding: 3px 5px 3px 5px; + color: var(--fg-gray); + background-color: white; + border-top-left-radius: 0; + border-top-right-radius: 5px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 5px; + border-top: 1px solid var(--blue-3); + border-left: 0; + border-right: 1px solid var(--blue-3); + border-bottom: 1px solid var(--blue-3); +} +#mode-device, #mode-profile { + max-width: 100px; + overflow: hidden; +} + +#doit { + display: none; + position: absolute; + white-space: nowrap; + font-size: smaller; + font-family: sans-serif; + margin-top: 5px; + padding: 5px; + top: 100%; + left: 50%; + transform: translateX(-50%); +} +#undo, #redo { + color: var(--main-color); + margin: 2px; + padding: 4px; +} +#undo:disabled, #redo:disabled { + color: rgba(0,0,0,.1) !important; + background-color: rgba(0,0,0,0.1) !important; +} +#undo:hover, #redo:hover { + color: black; + background-color: rgba(0,0,0,0.2); +} + +#lang-hold { + background-color: transparent; + position: absolute; + top: -35px; + left: -35px; + right: -35px; + bottom: -35px; +} +#lang-pop { + display: none; + position: absolute; + right: calc(100% + 2px); + top: 0; + padding: 1px; + text-align: right; + align-items: stretch; + white-space: nowrap; + background-color: #fff; + border: 1px solid var(--blue-2); + border-radius: 4px; + flex-direction: column; +} +#lang-pop > div { + padding: 2px 7px 2px 7px; +} +#lang-pop > div:hover { + background-color: var(--blue-4); +} + +#mid { + position: fixed; + top: 45px; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; +} + +.mod-print button, #catalogList button, #settingsList button { + text-transform: none !important; +} + +.slideshow #panel-left .settings { + transform: translateX(-100%); + transition: transform 0.1s ease-in-out; + transition-delay: 0.3s; +} +.slideshow #panel-left:hover .settings { + transform: translateX(0); + transition-delay: 0.0s; +} +.slideshow #panel-left:hover #slide-show { + transform: translateX(-100%); + transition-delay: 0.0s; +} +.slideshow #slide-show { + transition-delay: 0.5s; + transform: translateX(-80%); + /* transform: translateX(0); */ + position: absolute; + top: 5px; + left: 0; + bottom: 5px; + width: 25px; + background-color: var(--slide-bg); + border-top: 1px solid var(--blue-1); + border-right: 1px solid var(--blue-1); + border-bottom: 1px solid var(--blue-1); + border-bottom-right-radius: 4px; + border-top-right-radius: 4px; + z-index: -1; +} +.slideshow #slide-show span { + display: flex; +} +#slide-show { + flex-direction: row; + align-items: center; + justify-content: center;; +} +#slide-show span { + display: none; +} +#panel-right { + overflow: unset; +} +/* #panel-right { + direction: rtl; +} +#panel-right * { + direction: ltr; +} */ +#panel-left { + padding-right: 5px; +} +#panel-left, #panel-right { + max-height: 100%; + background-color: var(--work-area); +} +#panel-left, #panel-right, #modal { + pointer-events: visible; +} +#panel-left label, #panel-left svg, button svg { + pointer-events: none; +} +#ws-widgets button { + text-transform: none; + text-align: left; + white-space: nowrap; + align-items: center; + overflow: hidden; + max-width: 150px; +} +#ws-widgets button.selected { + background-color: var(--blue-3); + color: #000; +} +.widpop:not(:hover) .widopt { + display: none; +} +.widopt { + background-color: white; + border-radius: 3px; + position: absolute; + top: 0; + right: 100%; + bottom: 0; +} +.widopt button.disabled { + background-color: var(--blue-3); +} +#top-sep { + display: none; +} +#top-sep { + z-index: 14; + height: 1px; + min-height: 1px; + max-height: 1px; + background-image: var(--gradbar); +} +.line-sep2 { + z-index: 14; + height: 1px; + min-height: 1px; + max-height: 1px; + background-image: var(--gradbar2); +} + +.closer { + position: absolute; + top: 3px; + right: 6px; + z-index: 200; +} +.closer:hover { + color: #555; +} + +.tictac { + margin: 2px 2px 2px 0; + padding: 4px; + border-radius: 3px; + border: 1px solid white; + background-color: rgba(255,255,255,0.7); + display: flex; + align-items: center; + justify-content: center; +} +.tictac:hover { + border: 1px solid #ccc; + background-color: #eee; +} +.tictac.selected { + background-color: var(--main-color); + color: #fff; +} + +#float-tools { + position: absolute; + margin: 0; + top: 90px; + left: 50%; + transform: translateX(-50%); + z-index: 16; +} +#float-tools button { + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; +} + +#float-tools .menu { + margin-right: 5px; + border-right: 3px solid var(--main-color); +} +#float-tools .title { + min-height: 18px; +} + +#lab-axis, #lab-scale, #lab-size { + border-radius: 3px; + padding: 2px; + border: 1px solid white; +} +#lab-axis:hover, #lab-scale:hover { + background-color: var(--main-color); + color: white; +} + +/* top center toolbar */ +#render-tools { + pointer-events: all; + position: absolute; + gap: 2px; + top: 3px; + left: 0; + right: 0; + align-items: center; + justify-content: center; + flex-direction: row; +} +#render-tools span { + position: relative; +} +#render-tools svg { + font-size: 20px; + aspect-ratio: 1/1; + background-color: #fff; + border: 1px solid #aaa; + border-radius: 3px; + padding: 8px; +} +#render-tools svg:hover { + border: 1px solid blue; + background-color: var(--blue-4); +} +/** tool pop menus */ +#render-tools .pop:before { + position: absolute; + top: -10px; + left: -20px; + width: calc(100% + 40px); + height: calc(100% + 20px); + /* background-color: red; */ + content: ""; +} +#render-tools .pop { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 3px; + position: absolute; + margin-top: 3px; + padding: 3px; + top: 100%; + left: 50%; + transform: translateX(-50%); + text-align: center; + white-space: nowrap; +} +#render-tools > span:not(:hover) .pop { + display: none; +} +/** fdm extruder drop-down */ +#ft-nozzle .splat { + width: 10px; + height: 10px; + margin: 3px; + border-radius: 3px; + border: 1px solid #555; +} +#ft-nozzle > div { + flex-direction: row; + align-items: center; +} +/** selection rotation */ +#ft-rotate { + gap: 2px; + grid-template-rows: auto auto auto 1fr; + grid-template-columns: auto auto auto auto; +} +#ft-rotate label { + padding: 0 5px 0 5px; +} +#ft-rotate label, +#ft-rotate svg, +#fr-rotate div +{ + margin: 0; + text-align: center; + align-self: center; +} +#ft-rotate svg { + padding: 2px; + margin-top: 2px; + aspect-ratio: 2/1; + border-radius: 3px; + border: 1px solid transparent; +} +#ft-rotate > div:hover > svg { + background-color: var(--main-color); + color: white; +} +#ft-rotate > div { + display: flex; + align-items: center; + justify-content: center; +} +#ft-rotate .buttons { + align-items: stretch; + grid-column: 1/4; + min-width: 100px; +} +#ft-rotate button { + justify-content: center; +} +/** selection scaling */ +#ft-scale { + gap: 3px; + grid-template-rows: auto auto auto 1fr; + grid-template-columns: auto auto auto 1fr; + justify-content: stretch; + align-items: stretch; +} +#ft-scale > div { + align-items: center; + flex-direction: row; +} +#ft-scale > input { + margin: 0; +} +#ft-scale > div > label { + flex-grow: 1; + text-align: center; +} +#ft-scale > div > input { + position: absolute; + right: 0; +} +#ft-scale .buttons { + grid-column: 1/4 !important; +} +/** selection mesh actions */ +#ft-mesh-btn { + display: grid; + grid-auto-flow: row; + grid-template-rows: 1fr; +} +#ft-mesh-btn button { + justify-content: center; + flex-grow: 1; +} +#ft-mesh label { + font-size: smaller; +} +#ft-mesh .value { + font-family: monospace; + padding-left: 4px; +} +#mesh-info { + margin: 2px 1px 1px 1px; +} +#mesh-info label, #mesh-info span { + align-self: unset !important; + margin: 0; +} +#mesh-info span { + display: inline-flex; + align-items: center; +} +#mesh-info .em1 span { + min-width: 1em; +} +#mesh-info .em3 span { + min-width: 3.5em; +} +#lay-flat.selected { + background-color: var(--main-color); +} + +#act-animate.disabled { + color: #aaa !important; +} + +.dev-buttons { + margin: 1px -1px 0 -1px; +} +.dev-buttons button { + margin-left: 2px; + margin-right: 2px; + padding-bottom: 2px; +} +#dg .ext-buttons button { + cursor: pointer; +} +.ext-buttons button { + justify-content: center; + align-items: center; + text-align: center; + flex-grow: 1; + margin-top: 3px; + height: 75%; +} +.ext-buttons label { + font-size: smaller; + margin: 3px 0 3px 0; +} +.txt-sel { + background-color: #eee; +} +.txt-sel button, .btn-sel { + background-color: var(--highlight); +} + +#end { + left: 0; + right: 0; + bottom: 0; + position: fixed; + text-align: center; + font-size: smaller; +} + +#camop_dialog { + flex-direction: column; +} +#camop_dialog textarea { + border: 1px solid gray; + margin: 5px 1px 5px 1px; +} +#camop_dialog button, +#camop_dialog label +{ + text-align: center; + display: block; +} +#camops { + display: flex; + padding: 5px 0 0 0; +} +.cam-pop-op { + top: 50%; + right: calc(100% + 4px); + text-align: left; + position: absolute; + white-space: nowrap; + flex-direction: column; + background-color: #fff; + border: 1px solid #aaa; + border-radius: 5px; + margin-right: 6px; + padding: 5px 3px 5px 3px; + gap: 1.5px; +} +.pop-tics { + position: absolute; + left: -1px; + right: -1px; + bottom: 100%; + margin: 0 0 2px 0; +} +.pop-tics > div { + gap: 3px; + padding: 5px; + width: 100%; + max-width: 100%; + overflow: hidden; + font-size: 11px; + flex-direction: row; + background-color: rgba(255,255,255,0.6); + border: 1px solid rgba(0,0,0,0.25); + border-radius: 5px; + word-break: break-word; + white-space: normal; +} +#oplist { + gap: 3px; + margin-bottom: 3px; +} +#oplist .clock { + border: 0; + background-color: var(--blue-3); +} +#camops .set-sep { + margin-bottom: 4px; +} +#oplist .clock:hover { + background-color: var(--blue-2); +} +#oplist > div { + padding: 5px 20px 5px 7px !important; +} +#oplist > div, .opdiv { + display: flex; + flex-direction: column; + align-items: left; + justify-content: center; + border: 1px solid rgba(0,0,0,0.25); + border-radius: 4px; + padding: 5px 10px 5px 10px; + background-color: rgba(255,255,255,0.75); + white-space: nowrap; +} +#oplist > div:hover, #op-add:hover { + background-color: var(--blue-4); +} +#oplist > div.editing, #lzr-surface button.editing { + background-color: rgba(190,230,190,0.85); +} +#oplist .drag { + background-color: var(--highlight) !important; +} +#oplist .notime { + background-color: rgba(150,150,150,0.5) !important; +} +#oplist .disabled { + background-color: rgba(125,125,125,0.5) !important; +} +#oplist div .label { + pointer-events: none; +} +#oplist div .del { + position: absolute; + right: 6px; + font-size: 14px; + color: rgba(0,0,0,0.25); +} +#oplist div:hover .del { + color: #888; + z-index: 103; +} +#oplist div .del:hover { + color: #000; +} +#oplist div .opop { + position: absolute; + bottom: 100%; + white-space: pre; + font-family: monospace; + font-size: smaller; + text-align: left; + margin: 2px; + padding: 5px; + display: none; +} +#oplist div:hover .opop { + display: flex; +} +.mobile #oplist { + font-size: larger; +} +.mobile #op-add-pop { + font-size: larger; +} +#op-add-pop { + position: absolute; + display: none; + top: 0; + right: 100%; + transform: translateY(-50%); +} +#op-add:hover #op-add-pop { + display: flex; +} +.op-grid { + display: grid !important; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto; + align-items: center; + gap: 5px; +} +#op-add-list { + margin-right: 10px; + padding: 4px; + display: flex; + align-items: stretch; + flex-direction: column; + text-transform: none !important; +} +#op-add-list > div { + padding: 3px 5px 3px 5px; + display: flex; + flex-direction: column; + border: 1px dashed gray; + border-radius: 3px; + text-align: center; +} +#op-add-list > div:hover { + background-color: var(--blue-4); +} +#op-add-contour-menu, #op-add-register-menu { + position: absolute; + right: 0; + top: 0; +} +#cam-lathe:hover { + background-color: rgba(0,0,0,0) !important; +} + +#progress { + left: 0; + right: 0; + height: 6px; + min-height: 6px; + text-align: center; + background-color: rgb(220,220,220); + border-top: 1px solid #888; + border-bottom: 1px solid #888; +} +#progbar { + width: 0; + text-align: left; + position: relative; + background-color: rgb(255,90,90); +} +#progtxt { + display: none; + position: absolute; + left: 0; + right: 0; + top: 200%; + color: #888; + margin: 0 5px 0 5px; + padding: 0 5px 2px 5px; + white-space: nowrap; + flex-direction: row; + align-items: center; + justify-content: center; +} + +#dialog { + position: fixed; + outline: none; + color: black; + top: 100px; + left: 50%; + transform: translateX(-50%); + background-color: rgba(255,255,255,0.85); + border: 0; + border-top: 8px solid rgba(180,180,180,1); + border-bottom: 7px solid rgba(180,180,180,1); + border-radius: 5px; + overflow: hidden; + margin: 0; + padding: 5px; +} + +#modal { + display: none; + position: fixed; + z-index: 20; + top: 50%; + left: 0px; + right: 0px; + bottom: 0px; + height: 300px; + transform: translateY(-50%); + background-color: var(--bg-gray); + border-bottom: 1px solid black; + border-top: 1px solid black; + overflow: hidden; +} +#modal button, #modal select { + /* color: black !important; */ +} +#modal > div { + position: absolute; + transform: translateX(50%) translateY(-50%); + right: 50%; + top: 50%; + max-height: 80%; +} + +.mdialog { + display: none; + padding: 10px 20px 10px 20px; + min-height: 20px; + background-color: #fff; +} +.mod-top { + height: 10px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + background-color: var(--main-color); +} + +#tool-cols { + max-height: 350px; +} +.dark .mod-x { + border-color: #555; + background-color: rgba(0,0,0,0.15); +} +.mod-x { + border-radius: 5px; + border: 2px solid rgba(255,255,255,0.5); + background-color: rgba(255,255,255,0.5); + position: absolute; + z-index: 30; + bottom: -20px; + right: 0; +} +.mod-x svg { + aspect-ratio: 1 / 1 !important; +} +#mod-setup { + min-height: 550px; + max-height: 60%; + min-width: 725px; + max-width: 70%; + padding-bottom: 5px; +} +#mod-setup textarea { + border-radius: 3px; +} +#mod-setup .header { + text-align: center; + border-bottom: 1px solid rgba(126,153,183,1); + padding-bottom: 5px; + margin-bottom: 5px; + font-weight: bold; +} +#mod-setup .header.sub { + padding-top: 10px; +} +#mod-setup .left { + width: 180px; + padding-right: 10px; + margin-right: 10px; + border-right: 1px solid #ddd; +} +#mod-tools .set-header { + margin: 0; +} +#mod-help svg { + width: 36px; + height: 36px; + padding-right: 10px; + color: var(--main-color); +} +#mod-help a { + border-radius: 5px; + padding: 3px 10px 3px 5px; + display: flex; + align-items: center; + justify-content: flex-start; +} +.dark #mod-help a:hover { + color: #333; +} +#mod-help a:hover { + background-color: #eee; +} +#mod-help a:hover svg { + color: #123; +} +#mod-help .title { + font-weight: bold; + font-size: larger; + padding-bottom: 10px; +} +#kiri-version { + font-family: monospace; + padding-top: 5px; +} +.mod-print { + background-color: rgba(255,255,255,0.85); +} +.mod-print .grow button { + margin-left: 5px; +} +.mod-print .hint { + color: #345; + font-size: 12px; + font-style: italic; + text-align: center; + font-weight: normal; + padding-top: 5px; +} +.dark .mod-print .box { + background-color: rgba(0,0,0,0.15); + border-color: #555; +} +.mod-print .box { + background-color: rgba(255,255,255,0.5); + border: 1px solid #bbb; + border-radius: 3px; + padding: 12px 10px 10px 10px; +} +.mod-print .box select { + min-width: 10em; +} +.mod-print .header { + flex-direction: column; + justify-content: center; + align-items: center; + transform: translateY(50%); + z-index: 10; +} +.mod-print .header label { + background-color: white; + padding: 0 10px 0 10px; + border-radius: 5px; + border-right: 1px solid #bbb; + border-left: 1px solid #bbb; +} +.mod-print a { + text-decoration: underline; +} +.mod-print a:hover { + color: #567; + background-color: #ddd; +} +.mod-print label { + flex-grow: 1; + font-weight: bold; + padding-right: 10px; +} +.dark .mod-print button { + background-color: #555; + border-color: #777; +} +.mod-print button { + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 2em; +} +#print-canvas { + width: 180px; + height: 320px; + border: 1px solid #ccc; + background-color: #f5f5f5; + transform: scale(-1, 1); +} +#print-layer { + font-family: 'Russo One', sans-serif; + font-size: small; + width: 5em; + padding: 0 !important; + text-align: right; +} +#print-download { + min-height: 4em !important; +} +#print-palette { + display: none; +} +#code-preview-textarea { + min-width: 50em; + width: 100%; + height: 7em; + font-size: x-small; + font-family: Courier,monospace; + white-space: nowrap; + resize: none; +} +.mod-end { + height: 10px; + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + background-color: var(--main-color); +} +#settingsNew { + margin-top: 5px; + padding-top: 5px; + border-top: 1px solid var(--main-color); +} +#settingsName { + margin-right: 4px; + padding-right: 4px; + padding-left: 4px; + text-align: center; +} +#settingsList { + max-height: 450px; + overflow-y: auto; +} +#settingsList button.selected { + font-weight: bold; + background-color: #ccc; +} +#settingsList button.selected:hover { + background-color: #aaa; +} +#settingsList svg { + pointer-events: none; +} + +#help p { + margin: 2px; +} +#help a { + font-weight: bold; + text-decoration: underline; +} +@supports (-moz-appearance:none) { + #help li { + margin-left: 20px; + } + .content br { + margin: 10px; + } +} +#help li { + list-style-type: circle; + margin-left: 10px; +} +#help .header { + border: 1px solid rgba(0,0,0,0.75); + background-color: rgba(0,0,0,0.05); + border-radius: 2px; + justify-content: center; + white-space: nowrap; + text-align: center; + margin-bottom: 5px; + padding: 1px 0 2px 0; + font-family: monospace; + font-size: larger; + font-weight: bold; +} +#help .header2 { + border: 1px solid rgba(0,0,0,0.25); + background-color: rgba(0,0,0,0.03); + border-radius: 2px; + justify-content: center; + white-space: nowrap; + text-align: center; + margin-bottom: 5px; + padding: 1px 0 2px 0; + font-family: monospace; +} +#help .title { + text-align: center; + font-size: larger; + font-weight: bold; + padding: 10px 0 5px 50px; +} +#help .title label { + display: inline-flex; +} +#help #kiri-version { + position: absolute; + right: 10px; + bottom: 5px; + font-family: monospace; + font-weight: normal; + font-size: smaller; + color: #333; +} +#help .content { + flex-direction: column; + margin: 3px 0 8px 0; + line-height: 1.5em; +} +#help .content p { + text-align: justify; +} +#help .content li { + line-height: 1.75em; + padding-left: 22px ; + text-indent: -22px ; +} +#help .footer { + font-family: monospace; + align-items: center; + white-space: nowrap; + flex-direction: column; + padding: 8px 0 3px 0; + border-top: 1px dashed #aaa; +} +#help th, #help td { + font-family: monospace; +} +#help td label { + font-weight: bold; +} +#help button { + display: inline-flex; +} +.help-label { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + border: 1px solid rgba(0,0,0,0); + border-right: 0; + padding: 12px 10px 12px 8px; + z-index: 1000; +} +.help-label.sel { + background-color: white; + border: 1px solid #888; + border-right: 0; +} +#help-tabs { + width: 550px; + height: 450px; + transform: translateX(-1px); +} +#help-tabs p, #help-tabs li, #help-tabs table { + color: #035; +} +#help-tab-support a:hover, #help-tab-start a:hover { + background-color: rgba(61,133,198,0.25); + border-radius: 5px; +} +#help-start { + margin: 5px 0 5px 0; +} +#help-start a { + padding: 5px; +} +.help-tab { + flex-grow: 1; + display: none; + overflow-y: auto; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + border: 1px solid #888; + padding: 10px; + background-color: white; + white-space: normal; +} +.confirm { + padding-top: 15px; +} +.confirm > div { + padding-top: 10px; +} +.confirm label { + text-align: center; + line-height: 1.5em; + padding: 0 10px 0 10px; +} +.confirm button { + margin-left: 4px; +} +.confirm input { + padding: 3px; + margin: 0 10px 0 10px; + text-align: left; +} + +.dev-type { + font-family: 'Russo One', sans-serif; + flex-direction: column; + align-items: center; + background-color: rgba(255,255,255,0.75); + border-radius: 5px; + margin: 5px; + cursor: pointer; +} +.dev-type svg { + width: 60px; + height: 30px; +} +.dev-sel { + background-color: var(--highlight); +} + +.t-head { + font-weight: bold; +} +.t-33 { + width: 33%; +} +.flex { + display: flex; +} +.fat5 { + display: inline-block; + min-width: 5px !important; +} +.t-group { + background-image: var(--gradbar); + white-space: nowrap; + text-align: center; + justify-content: center; + border-radius: 2px; + padding: 1px 0 2px 0; + margin: 5px 0 5px 0; + color: white; +} +.t-body { + background-color: rgba(255,255,255,1); + border: 1px solid #bbb; + border-radius: 3px; +} +.t-inset { + padding: 3px; +} + +#devices { + width: 225px; +} +/* #device1, #device2, #palette3 { + width: 225px; +} */ +#palette3 { + display: none; +} +#dev-gcode textarea { + font-family: monospace; + overflow: auto; + padding: 5px; + width: 100%; + /* background-color: #f8f8f8; */ +} +#device-action button, #tool-action button { + flex-direction: column; + justify-content: center; +} +#catalogList { + min-width: 150px; + height: 350px; + overflow-y: scroll; +} +#catalogList .label { + width: 12em; + justify-content: flex-end; +} +#catalogList .rename { + font-style: italic; + color: #335; +} + +.dark #layer-animate button { + border: 1px solid #555; + background-color: #444; +} +.dark #layer-animate button:hover { + background-color: var(--main-color); +} +.dark #layer-animate button svg { + color: #ddd; +} +.dark #layer-animate label { + border: 1px solid #555; + background-color: #444; +} +.dark #layer-animate input { + border: 1px solid #444; +} +#layer-animate { + pointer-events: all; + position: absolute; + z-index: 200; + left: 0; + right: 0; + bottom: 0; + padding: 4px; +} +#layer-animate svg { + color: #333; + font-size: 30px; +} +#layer-animate .padleft { + margin-left: 8px !important; +} +#layer-animate button { + width: 45px; + align-items: center; + justify-content: center; +} +#layer-animate label { + display: inline-flex; + border-radius: 3px; + margin: 1px 2px 2px 1px; + padding: 2px; + justify-content: center; + align-items: center; + font-weight: bold; + border: 1px solid #ddd; + background-color: #eee; +} +#layer-animate input { + border-radius: 4px; +} + +/* #layer-toolpos { + position: absolute; + right: 3px; + bottom: 3px; + z-index: 201; +} +#layer-toolpos label, #layer-toolpos input { + display: inline-flex; + height: 30px; + border-radius: 3px; + border: 1px solid #ccc; + justify-content: center; + align-items: center; + font-weight: bold; + background-color: #ddd; +} +#layer-toolpos label { + width: 30px; + padding: 1px 1px 1px 1px; + margin: 1px 1px 2px 6px; +} +#layer-toolpos input { + background-color: #eee; + font-family: monospace; + padding: 1px 4px 1px 4px; + margin: 1px 1px 2px 1px; +} */ + +#layer-slider { + position: absolute; + font-size: smaller; + gap: 2px; + left: 15px; + right: 15px; + bottom: max(3px, env(safe-area-inset-bottom)); + pointer-events: all; +} +#slider.slider-mobile { + height: 40px !important; +} +#slider { + display: none; + height: 25px; + z-index: 100; +} +.mobile .sli-drag-el { + background-color: rgba(200,0,0,0.8) !important; +} +#slider-hi-val, #slider-lo-val { + display: none; + position: absolute; + font-family: monospace; + transform: translateY(-50%); + border: 1px solid #ddd; + bottom: 110%; + left: 50%; +} +#slider > div { + justify-content: center; + flex-direction: row; + align-items: center; +} +#slider-line { + position: absolute; + left: 0; + right: 0; + top: calc(50%-1.5px); + height: 3px; + min-height: 3px; + max-height: 3px; + background-color: #bbb; +} +#slider-zero { + z-index: 10; + background-color: #bbb; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; +} +#slider-max { + background-color: #bbb; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} +#slider-hold { + background-color: var(--blue-3); + position: absolute; + bottom: 0; + right: 0; + top: 0; +} +#slider-hold .slider-mobile { + width: 40px !important; +} +#slider-hold .handle { + width: 15px; + height: 100%; + background-color: var(--blue-1); +} +#slider-mid { + width: 20px; +} + +#top-slider { + position: absolute; + display: none; + top: 10px; + left: 35px; + right: 50px; + z-index: 1000; + pointer-events: all; +} +#anim-slider { + width: 100%; +} + +#alert-area { + display: none; + justify-content: center; + align-items: center; + position: absolute; + top: 90px; + right: 0; + left: 0; +} +#alert-border { + display: flex; + justify-content: center; + align-items: center; + max-width: 50%; + background-color: rgba(255,255,255,0.5); + border: 3px solid var(--bg-gray); + border-radius: 5px; +} +#alert-border:hover { + background-color: rgba(0,0,0,0.1); +} +#alert-text { + display: flex; + text-align: left; + font-weight: normal; + flex-direction: column; + margin: 3px 5px 3px 5px; + color: black; +} +#alert-text p { + margin: 5px; + text-align: center; +} + +#speeds { + display: none; + position: absolute; + height: 100%; + top: 0; + right: 100%; + bottom: 0; + border: 0; + margin: 0; + padding: 0; +} +#speedbar { + position: absolute; + top: 3px; + right: 3px; + background-color: transparent; +} +#speedbar label { + flex-grow: 1; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-family: monospace; + font-size: 12px; + padding: 0 5px 0 5px; + color: #fff; + margin: 0.5px; + border-radius: 3px; +} + +.settings { + max-height: 100%; + overflow-y: auto; + overflow-x: hidden; + align-items: stretch; + text-transform: capitalize; +} +.settings .set-header a { + font-size: smaller; +} +#dev-sel, .dev-sel { + margin-bottom: 0 !important; +} +#dev-list, .dev-list { + align-self: stretch; + padding: 0 5px 0 5px; +} +#dev-list:hover { + background-color: var(--blue-4); +} +.set2-group { + border-radius: 3px; + border: 1px solid #ddd !important; + background-color: #fff; + flex-flow: column; + padding: 4px; + margin: 2px; +} +.set2-group .var-row, .var-row { + background: #fff; + padding: 1px 4px 1px 4px; + margin: 0 !important; + border-left: 3px solid transparent; + border-right: 3px solid transparent; +} +.set2-group .var-row:hover, .var-row:hover { + border-right: 3px solid var(--blue-1); + background-color: var(--blue-4) !important; +} +.set2-group .var-row label, #mod-setup .var-row label { + margin-right: 25px; +} +.set2-sep { + background: #fff; + flex-shrink: 0; + flex-direction: column; + margin: 0 0 3px 0; +} +.pop-sep, .set-sep { + border: 0 !important; + padding: 0 !important; + height: 1px; + min-height: 1px; + max-height: 1px; + margin-top: 2px; + margin-bottom: 2px; + background-color: #bbb !important; +} +.set-header { + white-space: nowrap; + border-radius: 2px; + justify-content: left; + background-color: var(--blue-3); + border-left: 3px solid transparent; + border-right: 3px solid var(--main-color-hover); + border-bottom: 0.5px solid var(--main-color-hover); + padding: 4px; + margin: 0 0 4px 0; +} +.set-header.hidden { + margin-bottom: 0 !important; +} +.set-header > span { + color: var(--blue-2); + font-size: smaller; +} +.set-header-line { + margin-left: 5px; + width: 100%; +} +.set-pop { + top: 50%; + left: calc(100% + 6px); + position: absolute; + white-space: nowrap; + flex-direction: column; + transform: translateY(-50%); + background-color: #fff; + border: 1px solid #aaa; + border-radius: 5px; + margin-right: 5px; + padding: 5px; +} +.settings .ext-buttons { + margin: 0 4px 0 4px; + gap: 3px; +} +#layers .var-row { + border-radius: 10px; + background-color: #ddd; + padding: 2px 5px 2px 5px; + margin: 0 2px 3px 2px; +} +#layers button { + margin-left: 4px; + margin-top: 0; + margin-bottom: 3px; + padding-left: 9px; + padding-right: 9px; + border-radius: 10px; + font-weight: bold; + background-color: var(--highlight); +} +#layers button:hover { + background-color: var(--main-color); +} +.var-row { + white-space: nowrap; + align-items: center; + gap: 4px; +} +.var-row label { + padding: 2px 4px 2px 0; + flex-grow: 1; + font-size: smaller; + text-align: left; +} +.var-row .dim { + text-align: right; + font-size: smaller; + align-self: center; + justify-self: center; +} +.var-row input { + margin-right: 0; + margin-bottom: 1px; + padding-bottom: 1px; + padding-top: 1px; +} +.var-row button { + margin-right: 0; + margin-bottom: 1px; +} +.table-row { + flex-direction: row; + align-items: stretch; +} +.nounderline a { + text-decoration: none !important; + line-height: 2em; +} +.nounderline svg { + width: 30px !important; +} +.mono { + font-family: monospace; +} + +#fs-edit { + text-transform: none; + padding: 1px 3px 1px 3px; + margin: 0 3px 0 0 !important; +} +#gdpr { + position: absolute; + display: none; + bottom: 10px; + right: 10px; + z-index: 1000; + padding: 20px; + border-radius: 5px; + border: 1px solid black; + background-color: white; +} +#gdpr a { + text-decoration: underline; +} +#gotit { + font-weight: bold; + font-size: larger; + padding: 5px 20px 5px 20px; + margin-left: 10px; +} +#stats { + position: fixed; + font-size: 10px; + bottom: 5px; + right: 5px; + color: #000; + flex-direction: column; + align-items: flex-end; +} +#stats:hover #rnfo { + display: grid; +} +#rnfo { + display: none; + grid-template-columns: min-content 1fr; + position: absolute; + bottom: 100%; + gap: 3px; + right: 0; + background-color: rgba(255,255,255,0.05); + border: 1px solid black; + border-radius: 3px; + padding: 5px; +} +#rnfo label { + text-align: right; +} +#context-menu { + display: none; + position: absolute; + flex-direction: column; + padding: 4px 1px 4px 1px; + border: 1px solid #bbb; + border-radius: 3px; + background-color: rgba(255,255,255,0.85); +} +#context-menu button { + margin: 0; + padding: 10px; + border-radius: 0; + border: 1px solid rgba(255,255,255,0); + background-color: rgba(255,255,255,0); +} +#context-menu button:hover { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + background-color: #eee; + color: black; +} +#context-menu .sep { + border-top: 1px solid #ddd; + margin: 5px 0 5px 0; +} + +.range-info { + position: relative; +} +.range-info:hover .range-detail { + display: flex; +} +.range-detail { + position: absolute; + display: none; + flex-direction: column; + bottom: 50%; + right: 100%; + padding: 5px; + margin: 0 10px 0 10px; + font-size: smaller; + font-family: monospace; + transform: translateY(50%); + background-color: rgba(255,255,255,0.8); + border: 1px solid #aaa; + border-radius: 5px; +} +.range-detail label.pad { + padding-right: 8px; + border-bottom: 1px solid rgba(0,0,0,0.1); +} +.range-detail label.val { + font-weight: bold; +} +.range-detail span { + flex-grow: 1; +} diff --git a/web2/kiri/index.html b/web2/kiri/index.html new file mode 100644 index 00000000..78240eed --- /dev/null +++ b/web2/kiri/index.html @@ -0,0 +1,752 @@ + + + + + + + + + + + + + + + + + + + Kiri:Moto + + + + + + + + + + + + +
    + + diff --git a/web2/kiri/lang/da-dk.js b/web2/kiri/lang/da-dk.js new file mode 120000 index 00000000..07c912dd --- /dev/null +++ b/web2/kiri/lang/da-dk.js @@ -0,0 +1 @@ +da.js \ No newline at end of file diff --git a/web2/kiri/lang/da.js b/web2/kiri/lang/da.js new file mode 100644 index 00000000..63112b8c --- /dev/null +++ b/web2/kiri/lang/da.js @@ -0,0 +1,649 @@ +// Danish localization for Kiri:Moto +// Will defer to English map for any missing key/value pairs +kiri.lang['da'] = +kiri.lang['da-dk'] = { + version: "version", + enable: "enable", + settings: "indstillinger", + // common keys and menus + animate: "animer", // CAM animate button + arrange: "arranger", // layout workspace objects + axis: "ake", // left object scale pop menu + clear: "ryd", // clear workspace (remove all objects) + copy: "kopier", + delete: "slet", + detail: "detaljer", + done: "færdig", + enable: "aktiver", + export: "eksporter", + files: "filer", + help: "hjælp", + ghost: "spøgelse", // left render pop menu (wireframe) + hide: "skjul", // left render pop menu (invisible) + home: "hjem", + import: "importer", + language: "sprog", + machine: "maskine", // device or machine + metric: "måleenhed", + name: "navn", + prefs: "præferencer", // left menu "preferences" + preview: "forhåndsvisning", + recent: "nylig", + render: "render", // left render pop menu + reset: "Nulstil", + rotate: "roter", // left rotate pop menu + save: "gem", + scale: "skala", // left object scale pop menu + setup: "setup", + settings: "indstillinger", + size: "størrelse", + solid: "solid", // view type pop menu + tool: "værktøj", + tools: "værktøjer", // CAM tool menu button + top: "top", + type: "type", // CAM tool type + version: "version", + view: "vis", // left view pop menu + wire: "tråd", // left render pop menu + + acct_xpo: ["lav en sikkerhedskopi af din enhed","og enhedsprofiler med mulighed for at","medtage arbejdsområdeobjekter og positioner"], + + // RIGHT-CLICK CONTEXT MENU + rc_clws: "ryd arbejdsområde", + rc_xpws: "eksporter arbejdsområde", + rc_lafl: "læg flat", + rc_mirr: "spejlvend", + rc_dupl: "dupliker", + rc_xstl: "eksporter som STL", + + // DEVICE MENU and related dialogs + dm_sldt: "vælg en enhedstype", + dm_stdd: "standard enhed", + dm_mydd: "mine enheder", + dm_seld: "valgte enheder", + dm_rcnt: "nylige filer", + dm_savs: "gemte indstillinger", + dm_appp: "Programindstillinger", + + // CAM Tool Dialog + td_tyem: "ende", // end mill + td_tybm: "kugle", // ball mill + td_tytm: "konisk", // taper mill + td_tonm: "værktøj #", + td_shft: "skaft", // endmill shaft specs + td_flut: "spiral", // endmill flute specs + td_tapr: "konisk", // endmill taper specs + + // DEVICE dialog groups + dv_gr_dev: "enhed", + dv_gr_ext: "ekstruder", + dv_gr_out: "output", + dv_gr_gco: "gcode makroer", + + // DEVICE dialog (_s = label, _l = hover help) + dv_name_s: "navn", + dv_name_l: "enhedens navn", + dv_fila_s: "filament", + dv_fila_l: "diameter i millimeter", + dv_nozl_s: "dyse", + dv_nozl_l: "diameter i millimeter", + dv_bedw_s: "bredde", + dv_bedw_l: "arbejdsområde enheder", + dv_bedd_s: "dybde", + dv_bedd_l: "arbejdsområde enheder", + dv_bedh_s: "højde", + dv_bedh_l: ["max bygge højde","arbejdsområde enheder"], + dv_spmx_s: "maks. spindel", + dv_spmx_l: ["maks. spindel rpm","0 for at deaktivere"], + dv_xtab_s: "absolut positionering", + dv_xtab_l: "ekstruder bevægelser absolutte", + dv_orgc_s: "udgangspunkt i center", + dv_orgc_l: "plade har udgangspunkt i center", + // dv_orgt_s: "udgangspunkt i toppen", + // dv_orgt_l: "part z origin top", + dv_bedc_s: "rund plade", + dv_bedc_l: "enhedens plade er rund", + dv_belt_s: "bælte plade", + dv_belt_l: "kontinuerlig printplade", + dv_fanp_s: "blæser styrke", + dv_fanp_l: "bestem køle blæser styrke", + dv_prog_s: "fremgang", + dv_prog_l: "output for hver % fremgang", + dv_layr_s: "lag", + dv_layr_l: ["output fra hver","lag skifte"], + dv_tksp_s: "token mellemrum", + dv_tksp_l: ["tilføj et mellemrum mellem","gkode akseparametre","G0X0Y0X0","vs","G0 X0 Y0 Z0"], + dv_strc_s: "strip comments", + dv_strc_l: ["fjern gkode kommentarer","som begynder med ;"], + dv_fext_s: "filtypenavn", + dv_fext_l: "file name extension", + dv_dwll_s: "dwell", + dv_dwll_l: "gkode dvale kode", + dv_tool_s: "værktøj skift", + dv_tool_l: "værktøj skift kode", + dv_sspd_s: "spindel hastighed", + dv_sspd_l: "sæt spindel hastighed", + dv_paus_s: "pause", + dv_paus_l: "gkode pause kode", + dv_head_s: "sidehovede", + dv_head_l: "sidehovede gkode", + dv_foot_s: "sidefod", + dv_foot_l: "sidefod gkode", + dv_lzon_s: "tænd laser", + dv_lzon_l: "tænd laser gkode", + dv_lzof_s: "sluk laser", + dv_lzof_l: "sluk laser gkode", + dv_exts_s: "vælg", + dv_exts_l: "gkode for at aktivere denne ekstruder", + dv_dext_s: "fravalgt", + dv_dext_l: "gcode køres, før en anden ekstruder aktiveres", + dv_extd_s: "fravalgt", + dv_extd_l: "gkode for at fravælge denne ekstruder", + dv_exox_s: "forskydning x", + dv_exox_l: "dyse forskydning x", + dv_exoy_s: "forskydning y", + dv_exoy_l: "dyse forskydning y", + + // MODE + mo_menu: "tilstand", + mo_fdmp: "FDM Print", + mo_slap: "SLA Print", + mo_lazr: "Laser skær", + mo_cncm: "CNC fræs", + + // SETUP + su_menu: "setup", + su_devi: "Enheder", + su_tool: "Værktøjer", + su_locl: "Lokal", + su_xprt: "Eksport", + su_help: "Hjælp", + + // LOAD + fe_menu: "fil", + fn_recn: "Nylig", + fn_impo: "Importér", + + // FUNCTION + fn_menu: "handling", + fn_arra: "Arranger", + fn_slic: "Slice", + fn_prev: "Eksempel", + fn_expo: "Eksport", + + // VIEW + vu_menu: "vis", + vu_home: "hjem", + vu_rset: "Nulstil", + vu_sptp: "Top", + vu_spfr: "Front", + vu_splt: "Venstre", + vu_sprt: "Højre", + + // WORKSPACE + ws_menu: "vis", + ws_save: "Gem", + ws_cler: "Ryd", + + // OPTIONS + op_menu: "brugerflade", + op_disp: "Skærm", + op_xprt_s: "ekspert", + op_xprt_l: "vis flere indstillinger", + op_decl_s: "mærkater", + op_decl_l: "vis enhedsmærkater og logoer", + op_dang_s: "eksperimentel", + op_dang_l: "Vis eksperimentelle parametre", + op_hopo_s: "hold over pop op", + op_hopo_l: ["aktiver menu ved at","holde over den"], + op_dark_s: "mørkt tema", + op_dark_l: "mørkt tema brugerflade", + op_comp_s: "kompakt brugerflade", + op_comp_l: ["kompakt brugerflade","bedre til små skærme","og tablets"], + op_shor_s: "vis udgangspunkt", + op_shor_l: "vis enhedens eller proccens udgangspunkt", + op_shru_s: "vis linealer", + op_shru_l: ["vis akse linealer på større","gitterlinjer"], + op_sped_s: "vis hastigheder", + op_sped_l: ["vis hastighed til farve bar","i forhåndsvisning"], + op_auto_s: "auto fordel", + op_auto_l: ["automatisk layoutplatform,","når nye dele tilføjes"], + op_free_s: "frit layout", + op_free_l: ["tillad trækbart layout","ingen effekt i laser-tilstand"], + op_spcr_s: "afstand", + op_spcr_l: ["afstand mellem objekter","under automatisk layout","i arbejdsrumsenheder"], + op_invr_s: "vend zoom om", + op_invr_l: ["vender zoom på musens","hjul om"], + op_save_s: "auto gem", + op_save_l: ["beholder objekter i arbejdsområdet","mellem genindlæsninger"], + op_line_s: "linje type", + op_line_l: ["linjestil til gengivelse","af linjer i 3d\sti: 3d bedst","flad: 2d god","linje 1d hurtig"], + op_unit_s: "enheder", + op_unit_l: ["arbejdsområde enheder har effekt","på hatigheder og distancer"], + op_anim_s: "animer", + op_anim_l: ["animation mesh tæthed","højere er mere tæt","kræver mere hukommelse","og er langsommere"], + + lo_menu: "fordeling", + + pt_menu: "dele", + pt_deci_s: "decimer", + pt_deci_l: ["aktivere eller deaktivere punktdecimering","under portimport. for hurtigere hebandling","og lavere hukommelsesforbrug"], + pt_qual_s: "kvalitet", + pt_qual_l: ["detaljeringsniveau, der","skal bevares under behandling"," lavere er hurtigere"], + pt_heal_s: "reparer medeller", + pt_heal_l: ["forsøg på at reparere","ikke-manifold modeller","forøger slice tid"], + + xp_menu: "eksporteringer", + + // SETTINGS + se_menu: "profil", + se_load: "indlæs", + se_save: "gem", + + // FDM SLICING + sl_menu: "lag", + sl_lahi_s: "højde", + sl_lahi_l: ["højden af hver","lag i milimeter"], + ad_minl_s: "højde min", + ad_minl_l: ["adaptiv min laghøjde","i millimeter","skal være ikke-nul"], + sl_ltop_s: "top lag", + sl_ltop_l: ["antallet af faste lag","der skal lægges på","toppen af en print"], + sl_lsld_s: "faste lag", + sl_lsld_l: ["faste udfyldnigner beregnet","ud fra lag deltaen. se","lag pop op menuen"], + sl_lbot_s: "bund lag", + sl_lbot_l: ["antallet af faste lag","der skal lægges i","bunden af en print"], + ad_adap_s: "fleksibel", + ad_adap_l: ["brug adaptive laghøjder","med 'laghøjde' som maks","og 'lag min' som minimum"], + + // FDM SHELLS + sl_shel_s: "antal skaller", + sl_shel_l: ["antal omkredse","vægge, der skal genereres"], + sl_ordr_s: "skal rækkefølge", + sl_ordr_l: ["output shell rækkefølge","indvendigt til udvendigt","eller udvendigt til indvendigt","påvirker overfladekvaliteten"], + sl_strt_s: "lag start", + sl_strt_l: ["lagets startpunkt","sidste = sidste lag slut","center = del center","udgangspunkt = delens udgangspunkt"], + ad_thin_s: "tynde vægge", + ad_thin_l: ["opdage og udfylde huller","mellem vægge"], + + // FDM FILL + fi_menu: "fylde", + fi_type: "type", + fi_pcnt_s: "fylde brøkdel", + fi_pcnt_l: ["fylde densitet værdier","0.0 - 1.0"], + fi_angl_s: "startvinkel", + fi_angl_l: "bund vinkel i grader", + fi_over_s: "overlap", + fi_over_l: ["overlap as fyldet og skallen","som % af dysen bredde","højere binder bedre","0.0 - 1.0"], + // fi_rate_s: "print hastighed", + fi_rate_l: ["ekstruderingshastighed til fylde","indstillet til 0 for at bruge standard","output-printhastigheder"], + + // FDM FIRST LAYER + fl_menu: "bund", + fl_lahi_s: "laghøjde", + fl_lahi_l: ["højde af hver lag","i millimeter","skulle være >= laghøjde"], + fl_rate_s: "skal hastighed", + fl_rate_l: ["max hastighed på skaller","i millimeter / sekund"], + fl_frat_s: "fylde hastighed", + fl_frat_l: ["fylde max print hastighed","i millimeter / sekund"], + fl_mult_s: "print factor", + fl_mult_l: ["ekstrudering multiplikator","0.0 - 2.0"], + fl_sfac_s: "width factor", + fl_sfac_l: ["dyse strørrelse multiplikator","ændre linjeafstanden"], + fl_zoff_s: "lag forskydning", + fl_zoff_l: ["højde forskydning fra bælte","af laveste ekstrudering","i millimeter"], + fl_brim_s: "skygge størrelse", + fl_brim_l: ["tilføj skygge til delens bund","størrelsen er bredden i millimeter","0 for at deaktivere"], + fl_skrt_s: "antal skørter", + fl_skrt_l: ["antal første lag udvidelser","skørter at generere"], + fl_skro_s: "skirt offset", + fl_skro_l: ["skirt offset from part","in millimeters"], + fl_nozl_s: "dyse temp", + fl_nozl_l: ["grader i celsius","output indstillingen bruges","hvis denne er nul"], + fl_bedd_s: "plade temp", + fl_bedd_l: ["grader i celsius","output indstillingen bruges","hvis denne er nul"], + fr_spac_s: "flåde mellemrum", + fr_spac_l: ["ekstra afstand","mellem første lag og flåden","i millimeter"], + fr_nabl_s: "slå flåde til", + fr_nabl_l: ["lav en flåde under","modellen for bedre vedhæftning","bruger skørte afstand og","slår skørte fra"], + + // FDM BELT ONLY + fl_zoff_s: "bælte forskydning", + fl_zoff_l: ["højde forskudt fra bælte","af laveste ekstrudering","i millimeter"], + fl_brim_s: "skygge størrelse", + fl_brim_l: ["tilføj skygge til delens bund","størrelse er bredde i millimeter","0 for at deaktivere"], + fl_brmn_s: "skygge udløser", + fl_brmn_l: ["kun tilføje bremser, når segmentet","vendt mod bælet er kortere end dette","værdi i millimeter","0 = Uendelig"], + fl_bled_s: "model anker", + fl_bled_l: ["bælte model anker","ved start af print","i millimeter"], + + // FDM SUPPORT + sp_menu: "støtte", + sp_detect: "registrer", + sp_dens_s: "densitet", + sp_dens_l: ["brøkdel 0.0 - 1.0\anbefaledeet 0.15","0 for at deaktivere"], + sp_size_s: "søjle størrelse", + sp_size_l: ["søjle bredde","i millimeter"], + sp_offs_s: "del afstand", + sp_offs_l: ["afstand til del","i millimeter"], + sp_gaps_s: "mellemrum", + sp_gaps_l: ["antal lag","mellem støtte og del"], + sp_span_s: "max bro", + sp_span_l: ["maximum ikke-understøttet afstand"," der generere støtte","i millimeter"], + sp_angl_s: "max vinkel", + sp_angl_l: ["max overhæng vinkel før","støtte søjler genereres"], + sp_area_s: "min område", + sp_area_l: ["minimum område for","en støtte søjle","i millimeter"], + sp_xpnd_s: "udvid", + sp_xpnd_l: ["udvid støtte område","udover delen beyond part grænse","i millimeter"], + sp_nozl_s: "ekstruder", + sp_nozl_l: ["til multi-ekstruder systemer","hvor én ekstruder bruges","til støtte materiale"], + sp_auto_s: "automatisk", + sp_auto_l: ["aktiver genereret støtte\ved brug af 'at-slice-time' geometri","støtte vil først laves","efter slicing er færdig"], + + // LASER SLICING + ls_offs_s: "forskydning", + ls_offs_l: ["juster stråle bredden ","i millimeter"], + ls_lahi_s: "højde", + ls_lahi_l: ["laghøjde","i millimeter","0 = auto/opdag"], + ls_lahm_s: "min", + ls_lahm_l: ["layer height minimum","will merge auto slices","under this thickness","in millimeters"], + ls_sngl_s: "enkelt", + ls_sngl_l: ["lav kun én omgang","ved den specificerede laghøjde"], + + // CNC COMMON terms + cc_tool: "Værktøj", + cc_offs_s: "forskydning", + cc_offs_l: ["forskyd værktøj center"," fra valgt sti"], + cc_spnd_s: "spindel rpm", + cc_spnd_l: ["spindel hastighed i","revolutioner / minuttet"], + cc_sovr_s: "step over", + cc_sovr_l: ["as a brøkdel af","værktøjsdiameteren"], + cc_sdwn_s: "step ned", + cc_sdwn_l: ["step ned dybde","for hver omgang","i arbejdsområdets enheder","0 for at deaktivere"], + cc_feed_s: "hastighed", + cc_feed_l: ["max skærehastighed i"," arbejdsområdets enheder / minuttet"], + cc_plng_s: "dyk hastighed", + cc_plng_l: ["max z ake hastighed i","arbejdsområdets enheder / minuttet"], + cc_sngl_s: "vælg kun linjer", + cc_sngl_l: ["vælg kun enkelte kanter","i stedet for forbundede polyliner"], + + // CNC COMMON + cc_menu: "begrændsninger", + cc_flip: "flip", + cc_rapd_s: "xy hastighed", + cc_rapd_l: ["max xy rejse hastighed","i arbejdsområdets enheder / minuttet"], + cc_rzpd_s: "z hastighed", + cc_rzpd_l: ["max z rejse hastighed ","i arbejdsområdets enheder / minuttet"], + + cc_loff_s: "forskydning", + cc_loff_l: ["distance fra materialets flade","for planing","i arbejdsområdets enheder"], + + // CNC ROUGHING + cr_menu: "groft", + cr_lsto_s: "efterlad materiale", + cr_lsto_l: ["horizontal forskydning fra vertikal flade","materiale efterlades til efterbehandling","i arbejdsområdets enheder"], + cr_ease_s: "let ned", + cr_ease_l: ["dyk skæring vil","følge en spiralform ned eller","køre langsomt langs en linært sti"], + cr_clrt_s: "ryd top", + cr_clrt_l: ["kør en rydning over","grændseområdet af delen","ved z = 0"], + cr_clrp_s: "ryd hulrum", + cr_clrp_l: ["fræs ud gennem lommer","i stedet for kun omridset"], + cr_clrf_s: "ryd flader", + cr_clrf_l: ["interpolere trin ned for at","rydde eventuelle opdagede flade områder"], + cr_olin_s: "kun indersider", + cr_olin_l: ["begræns skæring til","indvendige delgrænser"], + + // CNC OUTLINE + co_menu: "omrids", + co_dogb_s: "dogbones", + co_dogb_l: ["indsæt dogbone skræinger","ind i indersidens hjørner"], + co_wide_s: "bred udskæring", + co_wide_l: ["udvide udskæringsstier","for deep cuts in hard material"], + co_olin_s: "kun indersider", + co_olin_l: ["begræns skæring til","indvendige delgrænser"], + co_olot_s: "kun ydersider", + co_olot_l: ["begrænse skæring til","udvendige delgrænser","som kan betragtes som skyggeomrids"], + co_omit_s: "udelade igennem", + co_omit_l: "eliminer gennemhulninger", + co_olen_s: "aktiver", + co_olen_l: "aktiver omrids skæring", + + // CNC CONTOUR + cn_menu: "kontur", + cf_angl_s: "max vinkel", + cf_angl_l: ["vinkler større end dette","betragtes som lodrette"], + cf_curv_s: "kun kurver", + cf_curv_l: ["begrænse lineær oprydning","til buede overflader"], + cf_olin_s: "kun indersider", + cf_olin_l: ["begræns skæring til","indvendige delgrænser"], + cf_linx_s: "aktiver y passering", + cf_linx_l: "lineær y-akse efterbehandling", + cf_liny_s: "aktiver x passering", + cf_liny_l: "lineær x-akse efterbehandling", + + // CNC TRACE + cu_menu: "spor", + cu_type_s: "type", + cu_type_l: ["følg = værktøjsspids følger linje","til højre eller venstre = værktøjsspids","følger linje forskudt efter værktøjsradius"], + + // CNC DRILLING + cd_menu: "bore", + cd_axis: "akse", + cd_points: "punkter", + cd_plpr_s: "dyk per", + cd_plpr_l: ["max dyk mellem","dvale perioden","i arbejdsområdets enheder","0 for at deaktivere"], + cd_dwll_s: "dvale tid", + cd_dwll_l: ["dvale tid","mellem dyk","i millisekunder"], + cd_lift_s: "løft bor", + cd_lift_l: ["løft mellem dyk","efter opholdsperiode","i arbejdsområdets enheder","0 for at deaktivere"], + cd_regi_s: "registrer", + cd_regi_l: ["bore registreringshuller","til dobbeltsidede dele","uafhængigt af aktivering af","boring, men bruger samme værktøj og indstillinger"], + + // CNC CUTOUT TABS + ct_menu: "tapper", + ct_angl_s: "vinkel", + ct_angl_l: ["startvinkel for tapmellemrum","i grader (0-360)"], + ct_numb_s: "antal", + ct_numb_l: ["antal tapper der skal bruges","bliver fordelt jævnt","omkring delen"], + ct_wdth_s: "bredde", + ct_wdth_l: "bredde i arbejdsområdets enheder", + ct_hght_s: "højde", + ct_hght_l: "højde i arbejdsområdets enheder", + ct_dpth_s: "dybde", + ct_dpth_l: ["distance arbejdsområdets enheder","som tappen projekteres fra","delens flade"], + ct_midl_s: "midtlinje", + ct_midl_l: ["brug midterlinjen på tappen","i stedet for z-bunden","til dobbeltsidet arbejde"], + ct_nabl_s: "auto", + ct_nabl_l: ["generer automatisk radiale tapper","projekteret fra delens center","ved hjælp af optælling og vinkelforskydning"], + + // OUTPUT + ou_menu: "output", + + // LASER KNIFE + dk_menu: "kniv", + dk_dpth_s: "skæredybde", + dk_dpth_l: ["endelig skæredybde","i millimeter"], + dk_pass_s: "skærepasseringer", + dk_pass_l: ["antal passeringer","ned for at skære dybde"], + dk_offs_s: "spids forskydning", + dk_offs_l: ["afstand fra knivspids","til midten af værktøjet","i millimeter"], + + // OUTPUT LASER + ou_spac_s: "mellemrum", + ou_spac_l: ["afstand mellem lagoutput","i millimeter"], + ou_scal_s: "skalering", + ou_scal_l: "multiplikator (0.1 to 100)", + ou_powr_s: "kraft", + ou_powr_l: ["0 - 100","repræsenterer %"], + ou_sped_s: "hastighed", + ou_sped_l: "millimeter / sekund", + ou_mrgd_s: "sammensæt", + ou_mrgd_l: ["flet alle lag ved hjælp af","farvekodning for at betegne","stabeldybde"], + ou_grpd_s: "grupperet", + ou_grpd_l: ["bevarer hvert lag som","en samlet gruppering","i stedet for adskilt","polygoner"], + ou_layr_s: "lagrækkefølge", + ou_layr_l: ["outputlagsrækkefølge","fra øverst til højre til","nederst til venstre"], + ou_layo_s: "lagfarve", + ou_layo_l: ["outputlagsfarver","for hvert z-indeks","overskrævet af sammensætning"], + ou_drkn_s: "træk kniv", + ou_drkn_l: ["aktiver trækkniv","output i gcode","skårne radier tilføjes","til hjørner med","nedskæringspasseringer"], + + // OUTPUT FDM + ou_nozl_s: "dyse temp", + ou_nozl_l: "grader i celsius", + ou_bedd_s: "plade temp", + ou_bedd_l: "grader i celsius", + ou_feed_s: "print hastighed", + ou_feed_l: ["max print hastighed","millimeter / sekund"], + ou_fini_s: "afslutningshastighed", + ou_fini_l: ["yderste skalhastighed","millimeter / sekund"], + ou_move_s: "print hastighed", + ou_move_l: ["rejsehastighed","millimeter / sekund","0 = aktiver G0 bevægelser"], + ou_shml_s: "skal faktor", + ou_flml_s: "solid faktor", + ou_spml_s: "fylde faktor", + ou_exml_l: ["ekstruderingsmultiplikator","0.0 - 2.0"], + // ou_fanl_s: "blæser lag", + // ou_fanl_l: "laget hvor blæseren aktiveres", + ou_fans_s: "blæser hastighed", + ou_fans_l: "0 - 255", + + // OUTPUT CAM + ou_toll_s: "tolerance", + ou_toll_l: ["overfladepræcision","i arbejdsområdets enheder","lavere er langsommere og","bruger mere hukommelse","0 = automatisk baseret","på animation præference"], + ou_zanc_s: "z anker", + ou_zanc_l: ["styrer delens position","når lager Z overstiger del Z"], + ou_ztof_s: "z forskydning", + ou_ztof_l: ["forskyd z anker","i arbejdsområdets enheder","har ingen effekt når","anker er i midten"], + ou_zbot_s: "z bottom", + ou_zbot_l: ["afstand fra delens bund"," til begrænset skære dybde","i arbejdsområdets enheder"], + ou_zclr_s: "z frihøjde", + ou_zclr_l: ["sikker rejse forskydning","fra toppen af delen","i arbejdsområdets enheder"], + ou_ztru_s: "z gennem", + ou_ztru_l: ["forlæng udskæringen nedad","i arbejdsområdets enheder"], + ou_conv_s: "konventionel", + ou_conv_l: ["fræsningsretning","fjern markeringen for 'stigning'"], + ou_depf_s: "dybde først", + ou_depf_l: ["optimer lommee skæringer"," med dybde prioritet"], + + // CAM STOCK + cs_menu: "materiale", + cs_wdth_s: "bredde", + cs_wdth_l: ["bredde (x) i arbejdsområdets enheder","0 er standard sat til delens størrelse"], + cs_dpth_s: "dybde", + cs_dpth_l: ["dybde (y) i arbejdsområdets enheder","0 er standard sat til delens størrelse"], + cs_hght_s: "højde", + cs_hght_l: ["højde (z) i arbejdsområdets enheder","0 er standard sat til delens størrelse"], + cs_offs_s: "forskydning", + cs_offs_l: ["brug bredde, dybde, højde"," som astand fra max","delstørrelse på platformen"], + cs_clip_s: "fastgør til", + cs_clip_l: ["groft og omrids","fastgør skærestier","til defineret materiale"], + cs_offe_s: "aktiver", + cs_offe_l: "aktiver fræsemateriale", + + // ORIGIN (CAM & LASER) + or_bnds_s: "udgangspunkt grænser", + or_bnds_l: ["udgangspunkt er relativt til","grænserne af alle objekter"], + or_cntr_s: "udgangspunkt center", + or_cntr_l: "udgangspunkt er refereret til fra centrum", + or_topp_s: "udgangspunkt top", + or_topp_l: "udgangspunkt er refereretfra toppen af objektet", + + // FDM ADVANCED + ad_menu: "ekpert", + ad_rdst_s: "tilbagetræk afstand", + ad_rdst_l: ["mængden af tilbagetrukket filament","for lange rejser. i millimeter"], + ad_rrat_s: "tilbagetræk hastighed", + ad_rrat_l: ["hastigheden på filamentet","i et tilbagetræk i mm/s"], + ad_rdwl_s: "påbegynd dvale", + ad_wpln_s: "tilbagetræk aftørring", + ad_wpln_l: ["ikke-printbevægelse","efter tilbagetræk","i millimeter"], + ad_rdwl_l: ["tid mellem genopstart af","filament og bevægelser","i millisekunder"], + ad_scst_s: "skal trykudligning", + ad_scst_l: ["ikke-printende ende","af en skal omkreds","i millimeter"], + ad_msol_s: "min solid", + ad_msol_l: ["minimumsareal (mm^2)","kræves for at holde solid","skal være > 0.1"], + ad_mins_s: "min hastighed", + ad_mins_l: ["minimum hastighed","for korte sektioner"], + ad_spol_s: "korte polygoner", + ad_spol_l: ["polygoner kortere end dette","har deres printhastighed","nedskaleret til min. hastighed","i millimeter"], + ad_arct_s: "bue tolerance", + ad_arct_l: ["konverter facetterede linjer til buer","center point drift tolerance","når matchende buepunkter","overvej værdier omkring 0,15","i millimeter","0 for at deaktivere"], + ad_zhop_s: "z hop dist", + ad_zhop_l: ["mængden z skal hæves\ikke tilbagetrækning bevægelser","i millimeter","0 for at deaktivere"], + ad_abkl_s: "anti-tilbageslag", + ad_abkl_l: ["brug mikrobevægelser til at annullere","tilbageslag mens der fyldes","i millimeter"], + ad_lret_s: "lag tilbagetræk", + ad_lret_l: ["tving filament tilbagetræk","mellem lag"], + ad_play_s: "polish layers", + ad_play_l: ["polish up to specified","# of layers at a time"], + ad_pspd_s: "polish speed", + ad_pspd_l: ["polishing speed","in millimeters / second"], + + // CAM EXPERT + cx_fast_s: "spring skygge over", + cx_fast_l: ["deaktiver detektering af overhæng","kan være hurtigere og bruge mindre","hukommelse med komplekse modeller","men mislykkes med udhæng","prøv at aktivere, hvis slicer","hænger under skygge"], + + // FDM GCODE + ag_menu: "gkode", + ag_nozl_s: "dyse", + ag_nozl_l: "vælg output dyse eller hoved", + ag_paws_s: "pause lag", + ag_paws_l: ["kommasepareret liste over lag","der skal indsættet pause kommandoer før"], + ag_loop_s: "loop lag", + ag_loop_l: ["lagintervaller, der skal gentages i formatet","første-sidste-optælling,første-sidste-optælling,...","udeladt antal = 1"], + + // SLA MENU + sa_menu: "slicing", + sa_lahe_s: "laghøjde", + sa_lahe_l: ["laghøjdet","i millimeter"], + sa_shel_s: "hul skal", + sa_shel_l: ["skaltykkelse i mm","Brug flere af lagets højde","brug 0 for solid (deaktiveret)"], + sa_otop_s: "åben top", + sa_otop_l: ["hvis skal er aktiveret","resulterer i en åben top"], + sa_obas_s: "åben bund", + sa_obas_l: ["hvis skal er aktiveret","resulterer i en åben bund","deaktiveret med understøtninger"], + + sa_layr_m: "lag", + sa_lton_s: "lys tændt tid", + sa_lton_l: ["lys tændt tid","tid i sekunder"], + sa_ltof_s: "lys slukket tid", + sa_ltof_l: ["lys slukket tid","tid i sekunder"], + sa_pldi_s: "skræl afstand", + sa_pldi_l: ["skræl afstand","i millimeter"], + sa_pllr_s: "afskalningshastighed", + sa_pllr_l: ["afskalningshastighed","i mm/sek"], + sa_pldr_s: "afskalningshastighed drop", + sa_pldr_l: ["afskalningshastighed drop","i mm/sek"], + + sa_base_m: "bund", + sa_balc_s: "antal lag", + sa_balc_l: ["antal","bundlag"], + sa_bltn_l: ["bundlag lys tændt","tid i sekunder"], + sa_bltf_l: ["bundlag lys slukket","tid i sekunder"], + + sa_infl_m: "fylde", + sa_ifdn_s: "densitet", + sa_ifdn_l: ["procent udfyldning","kræver skal","0 = deaktiveret","gyldig 0.0 - 1.0"], + sa_iflw_s: "linjebredde", + sa_iflw_l: ["luge linjebredde","i millimeter"], + + sa_supp_m: "støtte", + sa_slyr_s: "bundlag", + sa_slyr_l: ["bund støtte lag","værdiområde 0-10"], + sa_slgp_s: "mellemrum", + sa_slgp_l: ["antal lag mellem","flåde og bund på objektet"], + sa_sldn_s: "densitet", + sa_sldn_l: ["bruges til at beregne","antal støttesøjler","0.0-1.0 (0 = deaktiver)"], + sa_slsz_s: "størrelse", + sa_slsz_l: ["maks. størrelse på en","støtte søjle","i millimeter"], + sa_slpt_s: "punkter", + sa_slpt_l: ["antal punkter i","hver støttesøjle","i millimeter"], + sl_slen_l: "aktiver understøtte", + + sa_outp_m: "output", + sa_opzo_s: "z forskydning", + sa_opzo_l: ["z lagforskydning","almost always 0.0","0.0-1.0 in millimeters"], + sa_opaa_s: "anti alias", + sa_opaa_l: ["aktivere anti-aliasing","producerer større filer","kan sløre detaljer"] +}; diff --git a/web2/kiri/lang/de-de.js b/web2/kiri/lang/de-de.js new file mode 100644 index 00000000..ce527cf9 --- /dev/null +++ b/web2/kiri/lang/de-de.js @@ -0,0 +1,640 @@ +// German localization for Kiri:Moto +// Will defer to English map for any missing key/value pairs +kiri.lang['de'] = +kiri.lang['de-de'] = { + // common keys and menus + animate: "Animieren", // CAM animate button + arrange: "Anordnen", // layout workspace objects + axis: "Achse", // left object scale pop menu + clear: "alles entfernen", // clear workspace (remove all objects) + copy: "Kopieren", + delete: "Löschen", + detail: "Details", + done: "Fertig", + enable: "Aktivieren", + export: "Export", + files: "Dateien", + help: "Hilfe", + ghost: "Geist", // left render pop menu (wireframe) + hide: "Verbergen", // left render pop menu (invisible) + home: "home", + import: "Import", + language: "Sprache", + machine: "Maschine", // device or machine + metric: "Metrisch", + name: "Name", + prefs: "Einstellungen", // left menu "preferences" + preview: "Vorschau", + recent: "Letzte Objekte", + render: "Rendern", // left render pop menu + reset: "Zurücksetzen", + rotate: "Rotieren", // left rotate pop menu + save: "Speichern", + scale: "Skalieren", // left object scale pop menu + setup: "Einrichtung", + settings: "Einstellungen", + size: "Größe", + solid: "Undurchsichtig", // view type pop menu + tool: "Werkzeug", + tools: "Werkzeuge", // CAM tool menu button + top: "Oben", + type: "Typ", // CAM tool type + version: "Version", + view: "Ansicht", // left view pop menu + wire: "Drahtansicht", // left render pop menu + + acct_xpo: ["Lege ein Backup Deiner Geräte","und Geräte-Profile an, mit der","Option, den Arbeitsbereich und","mit Objekten und Anordnung zu speichern"], + + // RIGHT-CLICK CONTEXT MENU + rc_clws: "Arbeitsbereich freiräumen", + rc_xpws: "Arbeitsbereich exportieren", + rc_lafl: "Auf das Bett", + rc_mirr: "Spiegeln", + rc_dupl: "Duplizieren", + rc_xstl: "Als STL exportieren", + + // DEVICE MENU and related dialogs + dm_sldt: "Wähle einen Geräte-Typ aus", + dm_stdd: "Vordefinierte Geräte", + dm_mydd: "Meine Geräte", + dm_seld: "Ausgewähltes Gerät", + dm_rcnt: "Letzte Dateien", + dm_savs: "Gespeicherte Voreinstellungen", + dm_appp: "Anwendungseinstellungen", + + // CAM Tool Dialog + td_tyem: "end", // end mill + td_tybm: "ball", // ball mill + td_tytm: "Kegel", // taper mill + td_tonm: "Werkzeug #", + td_shft: "Schafft", // endmill shaft specs + td_flut: "Spannut", // endmill flute specs + td_tapr: "Kegel", // endmill taper specs + + // DEVICE dialog groups + dv_gr_dev: "Gerät", + dv_gr_ext: "Extruder", + dv_gr_out: "Output", + dv_gr_gco: "GCode Macros", + + // DEVICE dialog (_s = label, _l = hover help) + dv_name_s: "Name", + dv_name_l: "Geräte-Name", + dv_fila_s: "Filament", + dv_fila_l: "Durchmesser in mm", + dv_nozl_s: "Düse", + dv_nozl_l: "Durchmesser in mm", + dv_bedw_s: "Breite", + dv_bedw_l: "Maßeinheiten des Arbeitsbereichs", + dv_bedd_s: "Tiefe", + dv_bedd_l: "Maßeinheiten der Arbeitsfläche", + dv_bedh_s: "Höhe", + dv_bedh_l: ["Maximale Objekthöhe","in Maßeinheiten der Arbeitsfläche"], + dv_spmx_s: "Max. Spindelgeschwindigkeit", + dv_spmx_l: ["Maximale Umdrehungen pro Minute der Spindel","0 zum deaktivieren"], + dv_xtab_s: "Absolute Positionierung", + dv_xtab_l: "Filament wird mit absoluter Positionierung extrudiert", + dv_orgc_s: "Nullpunkt in der Mitte", + dv_orgc_l: "Nullpunkt des Druckbetts in der Mitte", + // dv_orgt_s: "origin top", + // dv_orgt_l: "part z origin top", + dv_bedc_s: "Rundes Druckbett", + dv_bedc_l: "Der Druckbereich des Gerätes ist rund", + dv_belt_s: "Riemen-Druckbett", + dv_belt_l: "Für bewegliche Druckbetten", + dv_fanp_s: "Lüfter-Einstellung", + dv_fanp_l: "Wie stark der Lüfter eingestellt sein soll", + dv_prog_s: "Fortschrittsanzeige", + dv_prog_l: "Wenn für jedes % der Fertigstellung eine Nachicht angezeigt werden soll", + dv_layr_s: "Ebene", + dv_layr_l: ["Ausgabe bei jeder","neuen Ebene"], + dv_tksp_s: "Befehls-Trenner", + dv_tksp_l: ["Füge ein Leerzeichen ein bei","GCode-Achsen-Befehlen","G0X0Y0X0","oder","G0 X0 Y0 Z0"], + dv_strc_s: "Kommentare entfernen", + dv_strc_l: ["Kommentare aus GCode entfernen","Kommentare beginnen mit ;"], + dv_fext_s: "Datei-Erweiterung", + dv_fext_l: "Datei-Erweiterung", + dv_dwll_s: "Verweilzeit", + dv_dwll_l: "gcode Verweilzeit script", + dv_tool_s: "Werkzeg-Wechel", + dv_tool_l: "Skript zum Werkzeugwechsel", + dv_sspd_s: "Spindelgeschwindigkeit", + dv_sspd_l: "Einstellung der Spindelgeschwindigkeit", + dv_paus_s: "Pause", + dv_paus_l: "Skript zum Ausführen bei einer Pause", + dv_head_s: "Anfang", + dv_head_l: "Skript für den Anfang einer GCode-Datei", + dv_foot_s: "Ende", + dv_foot_l: "Skript für das Ende einer GCode-Datei", + dv_lzon_s: "Laser an", + dv_lzon_l: "Skript, um den Laser einzuschalten", + dv_lzof_s: "Laser aus", + dv_lzof_l: "Skript, um den Laser auszuschalten", + dv_exts_s: "Auswählen", + dv_exts_l: "GCode, um diesen Extruder auszuwählen", + dv_dext_s: "Abwählen", + dv_dext_l: "GCode, bevor ein anderer Extruder ausgewählt wird", + dv_extd_s: "Deaktivieren", + dv_extd_l: "GCode, um diesen Extruder zu deaktivieren", + dv_exox_s: "X-Offset", + dv_exox_l: "X-Offset der Düse", + dv_exoy_s: "Y-Offset", + dv_exoy_l: "Y-Offset der Düse", + + // MODE + mo_menu: "Arbeits-Modus", + mo_fdmp: "FDM-Druck", + mo_slap: "SLA-Druck", + mo_lazr: "Laser-Cutting", + mo_cncm: "CNC-Fräsen", + + // SETUP + su_menu: "Setup", + su_devi: "Geräte", + su_tool: "Werkzeuge", + su_locl: "Lokal", + su_xprt: "Export", + su_help: "Hilfe", + + // LOAD + fe_menu: "Datei", + fn_recn: "Letzte", + fn_impo: "Import", + + // FUNCTION + fn_menu: "Modus", + fn_arra: "Anordnen", + fn_slic: "Slice", + fn_prev: "Vorschau", + fn_expo: "Export", + + // VIEW + vu_menu: "Ansicht", + vu_home: "Standard", + vu_rset: "Zurücksetzen", + vu_sptp: "Oben", + vu_spfr: "Vorn", + vu_splt: "Links", + vu_sprt: "Ansicht", + + // WORKSPACE + ws_menu: "Ansicht", + ws_save: "Speichern", + ws_cler: "Leeren", + + // OPTIONS + op_menu: "Interface", + op_disp: "Anzeige", + op_xprt_s: "Fortgeschritten", + op_xprt_l: "weitere Einstellungen", + op_decl_s: "decals", + op_decl_l: "show device decals and logos", + op_dang_s: "Experimentell", + op_dang_l: "Zeige experimentelle Parameter", + op_hopo_s: "hover pop", + op_hopo_l: ["enable menu hover","to activate"], + op_dark_s: "Dunkler Modus", + op_dark_l: "Dunkles Interface", + op_comp_s: "Kompakt", + op_comp_l: ["Kompakte Ansicht","Besser für kleine Bildschirme","und Tablets"], + op_shor_s: "Nullpunkt anzeigen", + op_shor_l: "Zeige den Nullpunkt des Gerätes oder des Prozesses", + op_shru_s: "Lineal einblenden", + op_shru_l: ["Zeige Lineale mit Maßen","auf den Hauptachsen"], + op_sped_s: "Geschwindigkeiten einblenden", + op_sped_l: ["Zeige Legende für Farben und Geschwindigkeiten","in der Vorschau"], + op_auto_s: "Automatische Anordnung", + op_auto_l: ["Ordne automatisch an","wenn neue Objekte hinzugefügt werden"], + op_free_s: "Freie Anordnung", + op_free_l: ["Erlaube Freie Anordnung von Objekten","Keine Auswirkung im Laser-Cutting Arbeitsmodus"], + op_spcr_s: "Abstand", + op_spcr_l: ["Abstand zwischen Objekten","bei automatischer Anordnung","in Maßeinheiten der Arbeitsfläche"], + op_invr_s: "Zoom Umkehren", + op_invr_l: ["Kehre die Richtung beim Mausrad-","Zoomen um"], + op_save_s: "Automatisch speichern", + op_save_l: ["Behalte Objekte im Arbeitsbereich","wenn die Anwendung neu geladen wird"], + op_line_s: "Darstellungs-Typ", + op_line_l: ["Wie Linien bei der Darstellung der Pfade gezeichnet werden","Beeinflußt 3D-Performance","Pfad: 3D (beste)","Flach: 2d (gut)","Linie = 1d (schnell)"], + op_unit_s: "Maßeinheiten", + op_unit_l: ["die Maßeinheit des Arbeitsbereich wirkt sich auf die Darstellung von ","Geschwindigkeiten und Entfernungen aus"], + op_anim_s: "Animieren", + op_anim_l: ["Dichte des Netzgitters beim Animieren","Je höher, desto dichter","benötigt mehr Speicher","und ist langsamer"], + + lo_menu: "Anordnung", + + pt_menu: "Objekte", + pt_deci_s: "Deizimierung", + pt_deci_l: ["Punktdezimierung","während des Imports. Für schnelleres Slicen","und weniger Speicherverbrauch"], + pt_qual_s: "Qualität", + pt_qual_l: ["Detailstufe beim","Slicen","geringer ist schneller"], + pt_heal_s: "Mesh reparieren", + pt_heal_l: ["Versuche","defekte Meshes zu reparieren","wirkt sich auf die Verarbeitungszeit us"], + + xp_menu: "Export", + + // SETTINGS + se_menu: "Profil", + se_load: "Laden", + se_save: "Speichern", + + // FDM SLICING + sl_menu: "Schichten", + sl_lahi_s: "Höhe", + sl_lahi_l: ["Höhe jeder Schicht","Schichthöhe in mm"], + ad_minl_s: "Mindesthöhe", + ad_minl_l: ["Mindesthöhe für adaptive Schichthöhe","in mm","Darf nicht null sein"], + sl_ltop_s: "top layers", + sl_ltop_l: ["number of solid layers","to enforce at the","top of the print"], + sl_lsld_s: "solid layers", + sl_lsld_l: ["solid fill areas computed","from layer deltas. see","layer pop menu"], + sl_lbot_s: "base layers", + sl_lbot_l: ["Anzahl der soliden Schichten","um die Unterseite des","Drucks zu verstärken"], + ad_adap_s: "Adaptive Höhe", + ad_adap_l: ["Verwende adaptive Schichthöhe","mit 'Maximale Höhe' als Maximum","und 'Mindesthöhe' als Minimum"], + + // FDM SHELLS + sl_shel_s: "Anzahl Außenwände", + sl_shel_l: ["Anzahl der äußeren","Wände, die erzeugt werden sollen"], + sl_ordr_s: "Druckreihenfolge der Wände", + sl_ordr_l: ["In welcher Reihenfolge die Wände gedruckt werden","innen nach aussen","oder aussen nach innen","wirkt sich auf die Oberflächenqualität aus"], + sl_strt_s: "Start der Ebene", + sl_strt_l: ["Startpunkt der Ebene","Letzte = Ende der letzten Ebene ","Mitte = Zur Mitte des Objekts","Nullpunkt = Zum Nullpunkt des Gerätes"], + ad_thin_s: "Dünne Wände", + ad_thin_l: ["Finde und fülle schmale Lücken","zwischen Außenwänden"], + + // FDM FILL + fi_menu: "Füllmaterial", + fi_type: "Typ", + fi_pcnt_s: "Füll-Dichte", + fi_pcnt_l: ["Füll-Dichte ","0.0 - 1.0"], + fi_angl_s: "Startwinkel für volle Füllung", + fi_angl_l: ["Startwinkel in Grad","90 Grad werden ","bei jeder folgender Schicht dazugerechnet","Wirkt sich nur auf vollgefüllte Schichten aus."], + fi_over_s: "Schicht-Überlappung", + fi_over_l: ["Überlappung von Schichten und Füllung","als Anteil des Düsen-Durchmessers","0.0 - 1.0"], + // fi_rate_s: "print speed", + fi_rate_l: ["Druckgeschwindigkeit für Füllmaterial","Auf 0 setzen, um die Standard-","Druckgeschwindigkeit zu verwenden"], + + // FDM FIRST LAYER + fl_menu: "Erste Schicht", + fl_lahi_s: "Schichthöhe", + fl_lahi_l: ["Höhe der ersten Schicht","in in mm","sollte >= der Standard-Schichthöhe sein"], + fl_rate_s: "Wand-Geschwindigkeit", + fl_rate_l: ["Maximalgeschwindigkeit beim Drucken von Wänden","in mm / s"], + fl_frat_s: "Füll-Geschwindikeit", + fl_frat_l: ["Maximalgeschwindikeit beim Drucken von Füllung","in mm / s"], + fl_mult_s: "Extrusions-Faktor", + fl_mult_l: ["Exktrusions-Faktor","0.0 - 2.0"], + fl_sfac_s: "Breiten-Faktor", + fl_sfac_l: ["Multipliziert mit dem Düsenfaktor","beeinflußt die Breite der Drucklinien"], + fl_skrt_s: "Anzahl der Skirt-Linien", + fl_skrt_l: ["Wieviele Linien um die erste Schicht","herum generiert werden sollen"], + fl_skro_s: "Skirt-Abstand", + fl_skro_l: ["Abstand des Skirts vom Objekt","in mm"], + fl_nozl_s: "Drucktemperatur", + fl_nozl_l: ["in Grad Celsius","Standard-Einstellung wird verwendet","wenn auf 0 gestellt"], + fl_bedd_s: "Betttemperatur", + fl_bedd_l: ["in Grad Celsius","Standard-Einstellung wird verwendet","when auf 0 gestellt"], + fr_spac_s: "Raft Abstand", + fr_spac_l: ["Zusätzlicher Abstand","zwischen erster Schicht und Raft","in mm"], + fr_nabl_s: "Raft verwenden", + fr_nabl_l: ["Erzeuge ein Raft unter dem","Objekt für bessere Betthaftung","verwendet Skirt-Abstand und","Deaktiviert die Erzeugung eines Skirt"], + + // FDM BELT ONLY + fl_zoff_s: "belt offset", + fl_zoff_l: ["height offset from belt","of lowest extrusion","in millimeters"], + fl_brim_s: "Größe des Brim", + fl_brim_l: ["Brim am Fuß des Objekts erzeugen","Größe in mm","0 um zu deaktivieren"], + fl_brmn_s: "Brim-Auslöser", + fl_brmn_l: ["Erzeuge nur Brim, wenn die Fläche","auf dem Gurt kürzer ist als dieser Wert","Angegeben in mm","0 = Immer"], + fl_bled_s: "Objekt verankern", + fl_bled_l: ["Objekt mit dem Gurt verankern","zu Beginn des Drucks","in mm"], + + // FDM SUPPORT + sp_menu: "Stützstrukturen", + sp_detect: "Analysieren", + sp_dens_s: "Dichte", + sp_dens_l: ["Anteil 0.0 - 1.0","empfohlen 0.15","0 zum deaktivieren"], + sp_size_s: "Breite der Säule", + sp_size_l: ["Breite der Säule","in mm"], + sp_offs_s: "Abstand", + sp_offs_l: ["Abstand vom Objekt","in mm"], + sp_gaps_s: "Schicht-Abstand", + sp_gaps_l: ["Anzahl der Schichten","zwischen Stützstruktur und Objekt"], + sp_span_s: "Maximale Spannbreite", + sp_span_l: ["Für Spannen über dieser Breite","werden Stützstrukturen erzeugt","in mm"], + sp_angl_s: "Maximaler Winkel", + sp_angl_l: ["Für Überhänge über diesem Winkel","werden Stützstrukturen erzeugt"], + sp_area_s: "Mindestfläche", + sp_area_l: ["Mindestfläche für","Eine Stützsäule","in mm"], + sp_xpnd_s: "Ausdehung", + sp_xpnd_l: ["Dehne die Stützstruktur","über die Objekt-Grenzen aus","in mm"], + sp_nozl_s: "Extruder", + sp_nozl_l: ["In Geräten mit mehreren Extrudern","welcher Extruder wird für","Stützmaterial verwendet"], + sp_auto_s: "Automatisch", + sp_auto_l: ["Erzeuge Stützstrukturen automatisch","beim Slicen","Stützstruktuen werden erst","Nach dem Slicen angezeigt"], + + // LASER SLICING + ls_offs_s: "Abstand", + ls_offs_l: ["Anpassung for Strahlbreite","in millimeters"], + ls_lahi_s: "Höhe", + ls_lahi_l: ["Schichthöhe","in mm","0 = auto/erkennen"], + ls_lahm_s: "min", + ls_lahm_l: ["Minimale Schichthöhe","Autoslices unter dieser Höhe","werden zusammengeführt","in millimeters"], + ls_sngl_s: "Einfach", + ls_sngl_l: ["Nur einen Slice bei","angegebener Schichthöhe ausführen"], + + // CNC COMMON terms + cc_tool: "Werkzeug", + cc_offs_s: "Versatz", + cc_offs_l: ["Versatz tool center","from chosen path"], + cc_spnd_s: "spindle rpm", + cc_spnd_l: ["spindle speed in","revolutions / minute"], + cc_sovr_s: "step over", + cc_sovr_l: ["as a fraction of","tool durchmesser"], + cc_sdwn_s: "step down", + cc_sdwn_l: ["step down depth","for each pass","in workspace units","0 to disable"], + cc_feed_s: "feed rate", + cc_feed_l: ["max cutting speed in","workspace units / minute"], + cc_plng_s: "plunge rate", + cc_plng_l: ["max z axis speed in","workspace units / minute"], + cc_sngl_s: "select lines only", + cc_sngl_l: ["select only single edges","instead of connected polylines"], + + // CNC COMMON + cc_menu: "Begrenzungen", + cc_flip: "Umdrehen", + cc_rapd_s: "xy feed", + cc_rapd_l: ["max xy moves feedrate","in workspace units / minute"], + cc_rzpd_s: "z feed", + cc_rzpd_l: ["max z moves feedrate","in workspace units / minute"], + + cc_loff_s: "Versatz", + cc_loff_l: ["distance from stock face","for leveling pass","in workspace units"], + + // CNC ROUGHING + cr_menu: "rough", + cr_lsto_s: "leave stock", + cr_lsto_l: ["horizontal Versatz from vertical faces","stock to leave for finishing pass","in workspace units"], + cr_ease_s: "ease down", + cr_ease_l: ["plunge cuts will","spiral down or ease","along a linear path"], + cr_clrt_s: "clear top", + cr_clrt_l: ["run a clearing pass over","the bounding area of the part","at z = 0"], + cr_clrp_s: "clear voids", + cr_clrp_l: ["mill out through pockets","instead of just the outline"], + cr_clrf_s: "clear faces", + cr_clrf_l: ["interpolate step down to","clear any detected flat areas"], + cr_olin_s: "inside only", + cr_olin_l: ["limit cutting to","inside part boundaries"], + + // CNC OUTLINE + co_menu: "outline", + co_dogb_s: "dogbones", + co_dogb_l: ["insert dogbone cuts","into inside corners"], + co_wide_s: "wide cutout", + co_wide_l: ["widen outside cutout paths","for deep cuts in hard material"], + co_olin_s: "inside only", + co_olin_l: ["limit cutting to","inside part boundaries"], + co_olot_s: "outside only", + co_olot_l: ["limit cutting to","exterior part boundaries","which can be thought of","as the shadow outline"], + co_omit_s: "omit through", + co_omit_l: "eliminate thru holes", + co_olen_s: "enable", + co_olen_l: "enabled outline cutting", + + // CNC CONTOUR + cn_menu: "contour", + cf_angl_s: "max angle", + cf_angl_l: ["angles greater than this","are considered vertical"], + cf_curv_s: "curves only", + cf_curv_l: ["limit linear cleanup","to curved surfaces"], + cf_olin_s: "inside only", + cf_olin_l: ["limit cutting to","inside part boundaries"], + cf_linx_s: "enable y pass", + cf_linx_l: "linear y-axis finishing", + cf_liny_s: "enable x pass", + cf_liny_l: "linear x-axis finishing", + + // CNC TRACE + cu_menu: "trace", + cu_type_s: "Typ", + cu_type_l: ["follow = tool tip follows line","right or left = tool tip","follows line offset by tool radius"], + + // CNC DRILLING + cd_menu: "drill", + cd_axis: "axis", + cd_points: "Punkte", + cd_plpr_s: "plunge per", + cd_plpr_l: ["max plunge between","dwell periods","in workspace units","0 to disable"], + cd_dwll_s: "Verweilzeit", + cd_dwll_l: ["Verweilzeit","between plunges in","in milliseconds"], + cd_lift_s: "drill lift", + cd_lift_l: ["lift between plunges","after dwell period","in workspace units","0 to disable"], + cd_regi_s: "register", + cd_regi_l: ["drill registration holes","for double-sided parts","independent of enable","drilling but uses same","tool and settings"], + + // CNC CUTOUT TABS + ct_menu: "Fräslasche", + ct_angl_s: "angle", + ct_angl_l: ["starting angle for tab spacing","in degrees (0-360)"], + ct_numb_s: "count", + ct_numb_l: ["number of tabs to use","will be spaced evenly","around the part"], + ct_wdth_s: "Breite", + ct_wdth_l: "width in workspace units", + ct_hght_s: "Höhe", + ct_hght_l: "height in workspace units", + ct_dpth_s: "Tiefe", + ct_dpth_l: ["distance in workspace units","that the tab projects from","the part surface"], + ct_midl_s: "Mittellinie", + ct_midl_l: ["use midline of tab","instead of z bottom","for double-sided work"], + ct_nabl_s: "auto", + ct_nabl_l: ["auto generate radial tabs","projected from part center","using count and angle offset"], + + // OUTPUT + ou_menu: "output", + + // LASER KNIFE + dk_menu: "knife", + dk_dpth_s: "cut depth", + dk_dpth_l: ["final cut depth","in millimeters"], + dk_pass_s: "cut passes", + dk_pass_l: ["number of passes","down to cut depth"], + dk_offs_s: "Spitze Versatz", + dk_offs_l: ["distance from blade tip","to center of tool","in millimeters"], + + // OUTPUT LASER + ou_spac_s: "spacing", + ou_spac_l: ["distance between layer output","in millimeters"], + ou_scal_s: "scaling", + ou_scal_l: "multiplier (0.1 to 100)", + ou_powr_s: "power", + ou_powr_l: ["0 - 100","represents %"], + ou_sped_s: "speed", + ou_sped_l: "millimeters / seconds", + ou_mrgd_s: "merged", + ou_mrgd_l: ["merge all layers using","color coding to denote","stacking depth"], + ou_grpd_s: "grouped", + ou_grpd_l: ["retain each layer as","a unified grouping","instead of separated","polygons"], + ou_layr_s: "layer order", + ou_layr_l: ["output layer order","from top right to","bottom left"], + ou_layo_s: "layer color", + ou_layo_l: ["output layer colors","for each z index","overridden by merged"], + ou_drkn_s: "drag knife", + ou_drkn_l: ["enable drag knife","output in gcode","cut radii are added","to corners with","cut down passes"], + + // OUTPUT FDM + ou_nozl_s: "Drucktemperatur", + ou_nozl_l: "In Grad Celsius", + ou_bedd_s: "Betttemperatur", + ou_bedd_l: "In Grad Celsius", + ou_feed_s: "Druckgeschwindigkeit", + ou_feed_l: ["Maximale Druckgeschwindigkeit","mm / s"], + ou_fini_s: "Außengeschwindigkeit", + ou_fini_l: ["Geschwindigkeit für Außenwände","mm / s"], + ou_move_s: "Bewegungsgeschwindigkeit", + ou_move_l: ["Geschwindigkeit, wenn nicht gedruckt wird ","mm / s","0 = aktiviere G0 Befehle"], + ou_shml_s: "Wand-Faktor", + ou_flml_s: "Voll-Füll-Faktor", + ou_spml_s: "Füll-Material-Faktor", + ou_exml_l: ["Extrusions-Multiplikator","0.0 - 2.0"], + ou_fans_s: "Geschwindigkeit der Bauteilkühlung", + ou_fans_l: "0 - 255", + + // OUTPUT CAM + ou_toll_s: "tolerance", + ou_toll_l: ["surface precision","in workspace units","lower is slower and","uses more memory","0 = automatic based","on animate preference"], + ou_zanc_s: "z anchor", + ou_zanc_l: ["controls the position of the part","when stock Z exceeds part Z"], + ou_ztof_s: "z offset", + ou_ztof_l: ["offset z anchor","in workspace units","has no effect when","anchor is middle"], + ou_zbot_s: "z bottom", + ou_zbot_l: ["offset from part bottom","to limit cutting depth","in workspace units"], + ou_zclr_s: "z clearance", + ou_zclr_l: ["safe travel offset","from top of part","in workspace units"], + ou_ztru_s: "z thru", + ou_ztru_l: ["extend cutout pass down","in workspace units"], + ou_conv_s: "Gegenlauffräsen", + ou_conv_l: ["milling direction","uncheck for 'climb'"], + ou_depf_s: "depth first", + ou_depf_l: ["optimize pocket cuts","with depth priority"], + + // CAM STOCK + cs_menu: "Rohteil", + cs_wdth_s: "width", + cs_wdth_l: ["width (x) in workspace units","0 defaults to part size"], + cs_dpth_s: "depth", + cs_dpth_l: ["depth (y) in workspace units","0 defaults to part size"], + cs_hght_s: "height", + cs_hght_l: ["height (z) in workspace units","0 defaults to part size"], + cs_offs_s: "Versatz", + cs_offs_l: ["use width, depth, height","as offsets from max","part size on platform"], + cs_clip_s: "clip to", + cs_clip_l: ["rough and outline","clip cutting paths","to defined stock"], + cs_offe_s: "Aktivieren", + cs_offe_l: "enable milling stock", + + // ORIGIN (CAM & LASER) + or_bnds_s: "origin bounds", + or_bnds_l: ["origin is relative to","boundary of all objects"], + or_cntr_s: "origin center", + or_cntr_l: "origin is referenced from the center", + or_topp_s: "origin top", + or_topp_l: "origin is references from the top of objects", + + // FDM ADVANCED + ad_menu: "Fortgeschritten", + ad_rdst_s: "Einzugslänge", + ad_rdst_l: ["Wie weit das Filament eingezogen werden soll","bei langen Bewegungen (in mm)"], + ad_rrat_s: "Einzugsgeschwindigkeit", + ad_rrat_l: ["Geschwindigkeit des Filamenteinzugs","in mm/s"], + ad_rdwl_s: "Vorschub-Pause", + ad_wpln_s: "Einzugs-Bewegung", + ad_wpln_l: ["Bewegung ohne Druck","nach Einzug","in millimeters"], + ad_rdwl_l: ["Zeitabstand zwischen","Bewegung und Vorschub nach Einzug ","in msec"], + ad_scst_s: "Außenwand-Coasting", + ad_scst_l: ["Welche Strecke am Ende einer","Außenwand nicht extrudiert wird","in mm"], + ad_msol_s: "Mindestfläche für vollständige Füllung", + ad_msol_l: ["Mindestfläche in mm^2 ","für vollständige Füllung","muß > 0.1 sein"], + ad_mins_s: "Mindestgeschwindigkeit", + ad_mins_l: ["Mindestgeschwindigkeit","für kurze Segmente"], + ad_spol_s: "Kurze Segmente", + ad_spol_l: ["Segmente die kürzer sind","werden mit","Mindestgeschwindigkeit gedruckt","in mm"], + ad_arct_s: "arc tolerance", + ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","in millimeters","0 to disable"], + ad_zhop_s: "Z Hop", + ad_zhop_l: ["Hebe Düse bei Einzug an","in mm","0 zum deaaktivieren"], + ad_abkl_s: "anti-backlash", + ad_abkl_l: ["Für schöneres Aussehen Flacher Oberflächen","verwende Micro-Bewegungen","um Backlash auszugleichen","in mm","0 zum deaktivieren","Wenn Deine Firmware M425 unterstützt","verwende das im Start-GCode und setze hier auf 0"], + ad_lret_s: "Einzug bei Ebenenwechsel", + ad_lret_l: ["Erzwinge Einzug","bei Ebenen wechseln"], + ad_play_s: "Ebenen glätten", + ad_play_l: ["glätte bis zu","dieser Anzahl Ebenen"], + ad_pspd_s: "Glätt-Geschwindigkeit", + ad_pspd_l: ["Glätt-Geschwindigkeit","in mm / s"], + + // CAM EXPERT + cx_fast_s: "skip shadow", + cx_fast_l: ["disable overhang detection","can be faster and use less","memory with complex models","but fails with overhangs","try enabling if slicing","hangs during shadowing"], + + // FDM GCODE + ag_menu: "GCode", + ag_nozl_s: "Düse", + ag_nozl_l: "Wählt Düse oder Druckkopf aus", + ag_paws_s: "Pause bei Ebenen", + ag_paws_l: ["Liste von Ebenen (kommasepariert","vor denen pausiert werden soll"], + ag_loop_s: "Schichten wiederholen", + ag_loop_l: ["Schichten die wiederholt werden sollen, im Format ","erste-letzte-anzahl,erste-letzte-anzahl,...","omitted count = 1"], + + // SLA MENU + sa_menu: "Slicen", + sa_lahe_s: "Schichthöhe", + sa_lahe_l: ["Schichthöhe","in mm"], + sa_shel_s: "Aushöhlen", + sa_shel_l: ["Wanddick mm","verwene Mehrfaches der Schichthöhe","setze 0 zum deaktivieren"], + sa_otop_s: "oben offen", + sa_otop_l: ["Wenn Aushöhlen aktiviert ist","bleibt das Objekt oben offen"], + sa_obas_s: "unten offen", + sa_obas_l: ["Wenn Aushöhlen aktiviert ist","bleibt das Objekt unten offen","deaktiviert, wenn Stützstrukturen verwendet werden"], + + sa_layr_m: "layers", + sa_lton_s: "light on time", + sa_lton_l: ["layer light on","time in seconds"], + sa_ltof_s: "light off time", + sa_ltof_l: ["layer light off","time in seconds"], + sa_pldi_s: "peel distance", + sa_pldi_l: ["peel distance","in millimeters"], + sa_pllr_s: "peel lift rate", + sa_pllr_l: ["peel lift speed","in mm/sec"], + sa_pldr_s: "peel drop rate", + sa_pldr_l: ["peel drop speed","in mm/sec"], + + sa_base_m: "base", + sa_balc_s: "layer count", + sa_balc_l: ["number of","base layers"], + sa_bltn_l: ["base layer light on","time in seconds"], + sa_bltf_l: ["base layer light off","time in seconds"], + + sa_infl_m: "Füllstrukturen", + sa_ifdn_s: "Dichte", + sa_ifdn_l: ["percent infill","requires shell","0 = disabled","valid 0.0 - 1.0"], + sa_iflw_s: "line width", + sa_iflw_l: ["hatch line width","in millimeters"], + + sa_supp_m: "support", + sa_slyr_s: "base layers", + sa_slyr_l: ["base support layers","value range 0-10"], + sa_slgp_s: "gap layers", + sa_slgp_l: ["number of layers between","raft and bottom of object"], + sa_sldn_s: "density", + sa_sldn_l: ["used to compute the","number of support pillars","0.0-1.0 (0 = disable)"], + sa_slsz_s: "size", + sa_slsz_l: ["max size of a","support pillar","in millimeters"], + sa_slpt_s: "Punkte", + sa_slpt_l: ["number of points in","each support pillar","in millimeters"], + sl_slen_l: "enable supports", + + sa_outp_m: "output", + sa_opzo_s: "z offset", + sa_opzo_l: ["z layer offset","almost always 0.0","0.0-1.0 in millimeters"], + sa_opaa_s: "anti alias", + sa_opaa_l: ["enable anti-aliasing","produces larger files","can blur details"] +}; diff --git a/web2/kiri/lang/de.js b/web2/kiri/lang/de.js new file mode 120000 index 00000000..bccb4c51 --- /dev/null +++ b/web2/kiri/lang/de.js @@ -0,0 +1 @@ +de-de.js \ No newline at end of file diff --git a/web2/kiri/lang/en-us.js b/web2/kiri/lang/en-us.js new file mode 120000 index 00000000..78e6f7f5 --- /dev/null +++ b/web2/kiri/lang/en-us.js @@ -0,0 +1 @@ +en.js \ No newline at end of file diff --git a/web2/kiri/lang/en.js b/web2/kiri/lang/en.js new file mode 100644 index 00000000..0e90a13a --- /dev/null +++ b/web2/kiri/lang/en.js @@ -0,0 +1,932 @@ +// English language map. Other localization maps will +// defer to English map for any missing key/value pairs +self.kiri.lang['en'] = +self.kiri.lang['en-us'] = { + // common keys and menus + animate: "animate", // CAM animate button + arrange: "arrange", // layout workspace objects + axis: "axis", // left object scale pop menu + back: "back", + clear: "clear", // clear workspace (remove all objects) + copy: "copy", + clone: "clone", // create duplicate + delete: "delete", + detail: "detail", + done: "done", + edit: "edit", + enable: "enable", + disable: "disable", + donate: "donate", + export: "export", + files: "files", + filter: "filter", + front: "front", + fullscreen: "fullscreen", + help: "help", + ghost: "ghost", // left render pop menu (wireframe) + hide: "hide", // left render pop menu (invisible) + home: "home", + import: "import", + language: "language", + left: "left", + machines: "machines", // device or machine + mesh: "mesh", // mesh editing + metric: "metric", + name: "name", + new: "new", // file new + off: "off", // turn something off (like a lightbulb) + on: "on", // turn something on (like a lightbulb) + profs: "profiles", // right menu "profiles" + prefs: "preferences", // right menu "preferences" + preview: "preview", + recent: "recent", + rename: "rename", + render: "render", // left render pop menu + reset: "reset", + right: "right", + rotate: "rotate", // left rotate pop menu + save: "save", + scale: "scale", // left object scale pop menu + select: "select", + setup: "setup", + settings: "settings", + size: "size", + slice: "slice", + solid: "solid", // view type pop menu + start: "start", + tool: "tool", + tools: "tools", // CAM tool menu button + top: "top", + type: "type", // CAM tool type + version: "version", + view: "view", // left view pop menu + wire: "wire", // left render pop menu + volume: "volume", // device build area + + acct_xpo: ["make a backup of your device","and device profiles with the","option to include workspace","objects and positions"], + + // RIGHT-CLICK CONTEXT MENU + rc_clws: "clear workspace", + rc_xpws: "export workspace", + rc_lafl: "lay flat", + rc_mirr: "mirror", + rc_dupl: "duplicate", + rc_focs: "set focus", + rc_heal: "heal meshes", + rc_swap: "replace vertices", + rc_xobj: "export OBJ", + rc_xstl: "export STL", + sb_info: ["print speed","in mm/s"], + rc_merg: "merge object meshes", + + // DEVICE MENU and related dialogs + dm_sldt: "select a device type", + dm_stdd: "standard devices", + dm_mydd: "my devices", + dm_seld: "selected device", + dm_rcnt: "recent files", + dm_savs: "settings", + dm_appp: "Application Preferences", + + // CAM Tool Dialog + td_tyem: "end", // end mill + td_tybm: "ball", // ball mill + td_tytm: "taper", // taper mill + td_tydr: "drill", // drill + td_tonm: "tool #", + td_shft: "shaft", // endmill shaft specs + td_flut: "flute", // endmill flute specs + td_tapr: "taper", // endmill taper specs + + // DEVICE dialog groups + dv_gr_dev: "device", + dv_gr_ext: "extruder", + dv_gr_out: "output", + dv_gr_gco: "gcode macros", + + // DEVICE dialog (_s = label, _l = hover help) + dv_name_s: "name", + dv_name_l: "device name", + dv_fila_s: "filament", + dv_fila_l: "diameter in millimeters", + dv_nozl_s: "nozzle", + dv_nozl_l: "diameter in millimeters", + dv_bedw_s: "width", + dv_bedw_l: "workspace units", + dv_bedd_s: "depth", + dv_bedd_l: "workspace units", + dv_bedh_s: "height", + dv_bedh_l: ["max build height","in workspace units"], + dv_rezx_s: "resolution X", + dv_rezx_l: ["x resolution"], + dv_rezy_s: "resolution Y", + dv_rezy_l: ["y resolution"], + dv_zmax_s: "Z axis speed limit", + dv_zmax_l: ["maximum Z axis speed","in millimeters / second","0 to disable"], + dv_time_s: "output time factor", + dv_time_l: ["modifier to print time estimates","in the gcode export dialog.","raise or lower value based on your","predicted vs observed print times"], + dv_spmx_s: "max spindle", + dv_spmx_l: ["max spindle rpm speed","0 to disable"], + dv_xtab_s: "absolute positioning", + dv_xtab_l: "extrusion moves absolute", + dv_orgc_s: "origin center", + dv_orgc_l: "bed origin center", + // dv_orgt_s: "origin top", + // dv_orgt_l: "part z origin top", + dv_bedc_s: "circular bed", + dv_bedc_l: "device bed is circular", + dv_belt_s: "belt bed", + dv_belt_l: "continuous printing bed", + dv_fsrc_s: "filament source", + dv_fsrc_l: ["how filament is fed\ninto the extruder"], + dv_retr_s: "firmware retract", + dv_retr_l: ["device fimware supports G10/G11"], + dv_fanp_s: "cooling", + dv_fanp_l: "set cooling fan power", + dv_feat_s: "feature", + dv_feat_l: "inject code for specified features", + dv_prog_s: "progress", + dv_prog_l: "output on each % progress", + dv_layr_s: "layer", + dv_layr_l: ["output at each","layer change"], + dv_tksp_s: "token spacer", + dv_tksp_l: ["add a space between","gcode axis parameters","G0X0Y0X0","vs","G0 X0 Y0 Z0"], + dv_strc_s: "strip comments", + dv_strc_l: ["strip gcode comments","comments begin with ;"], + dv_fext_s: "file extension", + dv_fext_l: "file name extension", + dv_lazr_s: "enable laser", + dv_lazr_l: ["enable laser operations"], + + dv_dwll_s: "dwell", + dv_dwll_l: "gcode dwell script", + dv_tool_s: "tool", + dv_tool_l: "tool change script", + dv_sspd_s: "spindle", + dv_sspd_l: "set spindle speed", + dv_paus_s: "pause", + dv_paus_l: "gcode pause script", + dv_head_s: "header", + dv_head_l: "gcode header script", + dv_foot_s: "footer", + dv_foot_l: "gcode footer script", + dv_pext_s: "exterior", + dv_pext_l: ["gcode script run before","printing exterior lines"], + dv_pint_s: "interior", + dv_pint_l: ["gcode script run before","printing interior lines"], + dv_lzon_s: "laser on", + dv_lzon_l: "gcode laser on script", + dv_lzof_s: "laser off", + dv_lzof_l: "gcode laser off script", + + dv_waon_s: "water on", + dv_waon_l: "gcode water pump on script", + dv_waof_s: "water off", + dv_waof_l: "gcode water pump off script", + + dv_dkon_s: "knife down", + dv_dkon_l: "gcode knife down script", + dv_dkof_s: "knife up", + dv_dkof_l: "gcode knife up script", + + dv_exts_s: "select", + dv_exts_l: "gcode run to enable this extruder", + dv_dext_s: "deselect", + dv_dext_l: "gcode run before enabling another extruder", + dv_extd_s: "deselect", + dv_extd_l: "gcode to deselect this extruder", + dv_exox_s: "offset x", + dv_exox_l: "nozzle offset x", + dv_exoy_s: "offset y", + dv_exoy_l: "nozzle offset y", + + dv_paid_s: "printer id", + dv_paid_l: ["unique printer identifier","may be made up"], + dv_paps_s: "ping spacing", + dv_paps_l: ["minimum filament consumption","between pings in millimeters","recommended 500 - 1000","0 = disable pings"], + dv_pafe_s: "feed length", + dv_pafe_l: ["feeder tube length in millimeters","short=570, medium=800, long=1100"], + dv_papl_s: "push length", + dv_papl_l: ["distance between extruder gear","and nozzle tip in millimeters"], + dv_paof_s: "offset length", + dv_paof_l: ["length to add or remove from","the first segment in millimeters","usually 0"], + dv_pahe_s: "heating time", + dv_pahe_l: ["offset from default"], + dv_paco_s: "cooling time", + dv_paco_l: ["offset from default"], + dv_pacm_s: "compression time", + dv_pacm_l: ["offset from default"], + + // MODE + mo_menu: "mode", + mo_fdmp: "FDM Print", + mo_slap: "SLA Print", + mo_lazr: "Laser Cut", + mo_cncm: "CNC Mill", + + // SETUP + su_menu: "setup", + su_devi: "Devices", + su_tool: "Tools", + su_locl: "Local", + su_xprt: "Export", + su_help: "Help", + + // LOAD + fe_menu: "file", + fn_recn: "Recent", + fn_impo: "Import", + + // FUNCTION + fn_menu: "action", + fn_arra: "Arrange", + fn_slic: "Slice", + fn_prev: "Preview", + fn_expo: "Export", + + // VIEW + vu_menu: "view", + vu_home: "Home", + vu_rset: "Reset", + vu_sptp: "Top", + vu_spfr: "Front", + vu_splt: "Left", + vu_sprt: "Right", + + // WORKSPACE + ws_menu: "view", + ws_save: "Save", + ws_cler: "Clear", + + // OPTIONS + op_menu: "interface", + op_disp: "display", + op_xprt_s: "expert", + op_xprt_l: "show more settings options", + op_decl_s: "show decals", + op_decl_l: "show device decals and logos", + op_shny_s: "detail render", + op_shny_l: "choose a slice render method that is slower and uses specular highlighting. this makes it easier to see detail in some models at the expense of render speed", + op_devl_s: "developer", + op_devl_l: ["show developer options","enable developer debugging"], + op_hopo_s: "hover pop", + op_hopo_l: ["enable menu hover","to activate"], + op_dark_s: "dark mode", + op_dark_l: "dark mode interface", + op_comp_s: "compact ui", + op_comp_l: ["compact user interface","better for small screens","and tablets"], + op_shor_s: "show origin", + op_shor_l: "show device or process origin", + op_shru_s: "show rulers", + op_shru_l: ["show axes rulers","on major gridlines"], + op_sped_s: "show speeds", + op_sped_l: ["show speed to color bar","in preview mode"], + op_auto_s: "auto layout", + op_auto_l: ["automatically layout platform","when new items added"], + op_free_s: "free layout", + op_free_l: ["permit dragable layout","no effect in laser mode"], + op_spcr_s: "spacing", + op_spcr_l: ["spacing between objects","during auto layout","in workspace units","in belt mode, use 0","to auto-space by height"], + op_spoa_s: "edge angle", + op_spoa_l: ["surface angle difference triggering an edge trace"], + op_spcx_s: "random X", + op_spcx_l: ["in belt mode, randomly","layout the part on X","to even belt wear"], + op_orth_s: "orthographic", + op_orth_l: ["orthographic display","requires page refresh"], + op_anta_s: "anti-alias", + op_anta_l: ["smooth display. better but slower","requires a page refresh or reload"], + op_invr_s: "invert zoom", + op_invr_l: ["invert mouse wheel","scroll zoom"], + op_save_s: "auto save", + op_save_l: ["preserve objects in workspace","between application reloads"], + op_line_s: "line type", + op_line_l: ["line style for path rendering","impacts 3d performance","path: 3d best","flat: 2d good","line = 1d fast"], + op_unit_s: "units", + op_unit_l: ["workspace units affects","speeds and distances"], + op_anim_s: "animate", + op_anim_l: ["animation mesh density","higher is more dense","takes more memory","and is slower"], + op_exgl_l: ["show Grid:Local devices in export dialog","includes Grid:Bots and OctoPrint plugins"], + op_exgh_l: ["legacy Grid:Host local network support"], + op_exop_l: ["legacy OctoPrint local network support","Grid:Space OctoPrint plugin recommended"], + + lo_menu: "layout", + + pt_menu: "slicer", + pt_deci_s: "decimate", + pt_deci_l: ["enable or disable point decimation","during port import. for faster slicing","and lower memory usage"], + pt_qual_s: "detail", + pt_qual_l: ["level of detail to retain during slicing operations. lower is faster and uses less memory. large scale models often do no benefit from higher detail. instead it my only result in memory and processing problems. reduce this value if you are getting out of memory errors."], + pt_heal_s: "heal mesh", + pt_heal_l: ["attempt to heal non-manifold meshes during slicing. may increase slicing time. enables a warning when non-manifold geometries are detected."], + pt_thrd_s: "threaded", + pt_thrd_l: ["use parallel processing when browser supports nested workers. may increase memory pressure on larger models. disable if you are getting out of memory errors"], + pt_assy_s: "assembly", + pt_assy_l: ["use web assembly when supported by browser. if you experience artifacts or memory errors, try disabling this."], + + xp_menu: "exports", + + // SETTINGS + se_menu: "profile", + se_load: "load", + se_save: "save", + + // FDM LAYERS + sl_menu: "paths", + sl_angl_s: "slice angle", + sl_angl_l: ["slice angle bias in degrees"], + sl_lahi_s: "layer height", + sl_lahi_l: ["height of each slice","layer in millimeters"], + ad_minl_s: "layer minimum", + ad_minl_l: ["adaptive min layer height","in millimeters","must be non-zero"], + ad_adap_s: "adaptive", + ad_adap_l: ["use adaptive layer heights","with 'layer height' as max","and 'layer min' as the min"], + sl_ltop_s: "top layers", + sl_ltop_l: ["number of solid layers at the top of the print or on flat areas facing up"], + sl_lsld_s: "solid layers", + sl_lsld_l: ["solid fill areas computed","from bridges and flat areas","detected betwen sliced layers"], + sl_lbot_s: "base layers", + sl_lbot_l: ["number of solid layers at bottom of the print or under bridge spans"], + + // FDM WALLS + sw_menu: "shells", + sl_shel_s: "shell count", + sl_shel_l: ["number of perimeter","walls to generate"], + ad_thin_s: "thin walls", + ad_thin_l: ["detect and fill gaps","between shell walls"], + + // FDM SHELLS + sl_line_s: "shell width", + sl_line_l: ["extrusion width in millimeters","0 = nozzle diameter"], + sl_ordr_s: "shell order", + sl_ordr_l: ["output shell order","inside to outside","or outside to inside","affects surface quality"], + sl_strt_s: "layer start", + sl_strt_l: ["layer starting point","last = last layer end","center = part center","origin = device origin"], + + // FDM FILL + fi_menu: "sparse fill", + fi_type: "fill type", + fi_pcnt_s: "fill amount", + fi_pcnt_l: ["fill density values","0.0 - 1.0"], + fi_rept_s: "fill repeat", + fi_rept_l: ["number of times to repeat","unchanged fill layers"], + fi_angl_s: "solid angle", + fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], + fi_grow_s: "solid expand", + fi_grow_l: ["expand projected solids","can help with solid areas","over sparse infill areas","units in millimeters"], + fi_over_s: "fill overlap", + fi_over_l: ["overlap with shell and other fill","for improved extrusion bonding","as fraction of nozzle diameter","0.0 - 2.0"], + // fi_rate_s: "print speed", + fi_rate_l: ["extrusion speed for infill","set to 0 to use default","output print speeds"], + + // FDM HEATING + fh_menu: "heating", + + // FDM COOLING + fc_menu: "cooling", + + // FDM FIRST LAYER + fl_menu: "base", + fl_lahi_s: "layer height", + fl_lahi_l: ["height of each slice","in millimeters","should be >= slice height"], + fl_rate_s: "shell speed", + fl_rate_l: ["shell printing max speed","in millimeters / second"], + fl_frat_s: "fill speed", + fl_frat_l: ["fill printing max speed","in millimeters / second"], + fl_mult_s: "flow factor", + fl_mult_l: ["extrusion multiplier","0.0 - 2.0"], + fl_sfac_s: "width factor", + fl_sfac_l: ["multiplier on nozzle size","changes line spacing"], + fl_skrt_s: "skirt count", + fl_skrt_l: ["number of first-layer offset","brims to generate"], + fl_skro_s: "skirt offset", + fl_skro_l: ["skirt offset from part","in millimeters","when rafts are used","this is the raft offset"], + fl_nozl_s: "nozzle temp", + fl_nozl_l: ["degrees in celsius","output setting used","when this is zero"], + fl_bedd_s: "bed temp", + fl_bedd_l: ["degrees in celsius","output setting used","when this is zero"], + fr_spac_s: "raft gap", + fr_spac_l: ["additional layer spacing","between 1st layer and raft","in millimeters"], + fr_nabl_s: "raft enable", + fr_nabl_l: ["create a raft under the","model for better adhesion","uses skirt offset and","disables skirt output"], + fr_draf_s: "draft shield", + fr_draf_l: ["extend skirt to height of part","which acts as a shield against","drafts and traps hot air"], + + // FDM BELT ONLY + fl_zoff_s: "belt offset", + fl_zoff_l: ["height offset from belt","of lowest extrusion","in millimeters"], + fl_flat_s: "flatten", + fl_flat_l: ["flatten to the belt any point","height less than this value","in millimeters"], + fl_brim_s: "brim sides", + fl_brim_l: ["add brim to part perimeter","size is width in millimeters","0 to disable"], + fl_brin_s: "brim interior", + fl_brin_l: ["add brim to part interior","when interior voids exist","size is width in millimeters","0 to disable"], + fl_brmn_s: "brim trigger", + fl_brmn_l: ["add brims only when segment","facing belt is shorter than this","value in millimeters","0 = Infinity"], + fl_brco_s: "brim comb", + fl_brco_l: ["add brim gaps","every n lines","0 to disable"], + fl_brgp_s: "brim gap", + fl_brgp_l: ["spacing between brim and part","in millimeters"], + fl_bled_s: "part anchor", + fl_bled_l: ["belt part anchor","at start of print","in millimeters"], + fl_blmp_s: "anchor bump", + fl_blmp_l: ["add a bump to the start of","an anchor to help it peel","off at the end of the belt","in millimeters"], + + // FDM SUPPORT + sp_menu: "support", + sp_detect: "detect", + sp_dens_s: "density", + sp_dens_l: ["fraction 0.0 - 1.0","recommended 0.15","0 to disable"], + sp_size_s: "pillar size", + sp_size_l: ["pillar width in millimeters","smaller values exponentially","increase detection times"], + sp_offs_s: "part offset", + sp_offs_l: ["offset from part","in millimeters"], + sp_gaps_s: "gap layers", + sp_gaps_l: ["number of layers","offset from part"], + sp_span_s: "max span", + sp_span_l: ["unsupported span that causes","a new support to be generated","in millimeters"], + sp_angl_s: "max angle", + sp_angl_l: ["max overhang angle before","supporting pillar is generated","normal values 45-60","belt values 0-10"], + sp_area_s: "min area", + sp_area_l: ["minimum area for","a support column","in millimeters","smaller values will","slow detection"], + sp_xpnd_s: "expand", + sp_xpnd_l: ["expand support area","beyond part boundary","in millimeters"], + sp_grow_s: "grow", + sp_grow_l: ["grow support column to fill small gaps","in millimeters"], + sp_nozl_s: "extruder", + sp_nozl_l: ["in multi-extruder systems","the extruder to use for","support material"], + sp_outl_s: "enclosed", + sp_outl_l: ["enclose support pattern","inside a perimeter shell"], + sp_auto_s: "automatic", + sp_auto_l: ["enable generated supports","using at-slice-time geometry","supports will only appear","after slicing completes"], + + // CNC COMMON terms + cc_tool: "tool", + cc_offs_s: "offset", + cc_offs_l: ["offset tool center","from chosen path"], + cc_spnd_s: "spindle rpm", + cc_spnd_l: ["spindle speed in","revolutions / minute"], + cc_sovr_s: "step over", + cc_sovr_l: ["a fraction of tool diameter between 0 and 1. values greater than 0.49 may result in uncut areas or undesirable artifacts."], + cc_sovc_s: "wide steps", + cc_sovc_l: ["number of wide cutout steps"], + cc_sang_s: "step angle", + cc_sang_l: ["rotation in degrees between lathe axial points"], + cc_sdwn_s: "step down", + cc_sdwn_l: ["step down depth","for each pass","in workspace units","0 to disable"], + cc_thru_s: "cut thru", + cc_thru_l: ["additional cut depth below trace bottom"], + cc_offd_s: "offset", + cc_offd_l: ["overrides default offset distance (which is the tool radius) when set to any value but zero"], + cc_feed_s: "feed rate", + cc_feed_l: ["max cutting speed in","workspace units / minute"], + cc_plng_s: "plunge rate", + cc_plng_l: ["max z axis speed in","workspace units / minute"], + cc_sngl_s: "select lines only", + cc_sngl_l: ["select only single edges","instead of connected polylines"], + cc_sele_s: "select", + cc_sele_l: ["select lines for follow","select loops for clear","select surface for pocket"], + + // CNC COMMON + cc_menu: "limits", + cc_rapd_s: "xy feed", + cc_rapd_l: ["max xy moves feedrate","in workspace units / minute"], + cc_rzpd_s: "z feed", + cc_rzpd_l: ["max z moves feedrate","in workspace units / minute"], + + // CNC LEVELING + cc_loff_s: "z offset", + cc_loff_l: ["distance from stock face","in workspace units"], + cc_lxyo_s: "xy offset", + cc_lxyo_l: ["xy offset from part boundary as a fraction of the tool diameter (0 - 1.0)"], + cc_lsto_s: "stock", + cc_lsto_l: ["use stock area instead of part area"], + + // CNC ROUGHING + cr_menu: "rough", + cr_lsto_s: "leave stock", + cr_lsto_l: ["horizontal offset from vertical faces","stock to leave for finishing pass","in workspace units"], + cr_lstz_s: "leave stock z", + cr_lstz_l: ["vertical offset for roughing passes in workspace units. best for roughing curved surfaces."], + cr_ease_s: "ease down", + cr_ease_l: ["plunge cuts will","spiral down or ease","along a linear path"], + cr_clst_s: "clear stock", + cr_clst_l: ["in indexed mode, clear entire stock area, not just to part periemter"], + cr_clrt_s: "clear top", + cr_clrt_l: ["run a clearing pass over","the bounding area of the part","at z = 0"], + cr_clrp_s: "clear voids", + cr_clrp_l: ["mill out through pockets","instead of just the outline"], + cr_clrf_s: "clear faces", + cr_clrf_l: ["interpolate step down to","clear any detected flat areas"], + cr_olin_s: "inside only", + cr_olin_l: ["limit cutting to","inside part boundaries"], + + // CNC OUTLINE + co_menu: "outline", + co_merg_s: "merge overlap", + co_merg_l: ["merge overlapping lines to prevent overcutting into adjacent solids"], + co_dogb_s: "dogbones", + co_dogb_l: ["insert dogbone cuts","into inside corners"], + co_dogr_s: "reverse bones", + co_dogr_l: ["reverse dogbone direction"], + co_clrt_s: "clear top", + co_clrt_l: ["cut starting at the top of stock","when stock is enabled"], + co_wide_s: "wide cutout", + co_wide_l: ["widen outside cutout paths","for deep cuts in hard material"], + co_olin_s: "inside only", + co_olin_l: ["limit cutting to","inside part boundaries"], + co_olot_s: "outside only", + co_olot_l: ["limit cutting to","exterior part boundaries","which can be thought of","as the shadow outline"], + co_omit_s: "omit through", + co_omit_l: "eliminate thru holes", + co_omvd_s: "omit pocket", + co_omvd_l: "eliminate interior pockets", + co_olen_s: "enable", + co_olen_l: "enabled outline cutting", + + // CNC CONTOUR + cn_menu: "contour", + cf_angl_s: "max angle", + cf_angl_l: ["changes in z that result in angles greater than this are considered vertical and omitted from curves"], + cf_leav_s: "leave stock", + cf_leav_l: ["amount of stock to leave above part surface"], + cf_botm_s: "z bottom", + cf_botm_l: ["obey z bottom limit"], + cf_curv_s: "curves only", + cf_curv_l: ["limit linear cleanup","to curved surfaces"], + cf_olin_s: "inside only", + cf_olin_l: ["limit cutting to","inside part boundaries"], + cf_linx_s: "enable y pass", + cf_linx_l: "linear y-axis finishing", + cf_liny_s: "enable x pass", + cf_liny_l: "linear x-axis finishing", + + // CNC TRACE + cu_menu: "trace", + cu_type_s: "type", + cu_type_l: ["clear = clear a closed-line pocket","follow = tool tip follows line","with offset option when closed"], + + // CNC POCKET + cp_xpnd_s: "expand", + cp_xpnd_l: ["grow (+) or shrink (-) selected areas","contour uses entire selected area","non-contour defaults to tool radius","in workspace units"], + cp_smoo_s: "smooth", + cp_smoo_l: ["clean up jagged edges caused by angled walls. can speedup and improve pocketing for detailed geometries. recommend starting with values around 1 if slicing takes too long or the contours look jagged"], + cp_foll_s: "selection", + cp_foll_l: ["curvature selection limit in degrees. values closer to 0 will restrict selections to flatter surfaces"], + cp_refi_s: "refine", + cp_refi_l: ["number of refining passes to perform on contoured polylines. meant to address Z delta sawtoothing caused by faced geometries whereas smoothing handles XY. values > 10 work well for gently contoured geometries with significant Z movement."], + cp_cont_s: "contour", + cp_cont_l: ["ignore interior voids and features"], + cp_engr_s: "engrave", + cp_engr_l: ["sets up a single pass around the perimeter of the selected area. also useful for 3D laser marking"], + cp_outl_s: "outline only", + cp_outl_l: ["ignore interior voids and features"], + + // CNC DRILLING + cd_menu: "drill", + cd_axis: "axis", + cd_points: "points", + // cd_plpr_s: "plunge per", + // cd_plpr_l: ["max plunge between","dwell periods","in workspace units","0 to disable"], + cd_dwll_s: "dwell time", + cd_dwll_l: ["dwell time","between plunges in","in milliseconds"], + cd_lift_s: "drill lift", + cd_lift_l: ["lift between plunges","after dwell period","in workspace units","0 to disable"], + cd_regi_s: "register", + cd_regi_l: ["drill registration holes","for double-sided parts","independent of enable","drilling but uses same","tool and settings"], + cd_thru_s: "z extend", + cd_thru_l: ["extend downward cut through stock","in workspace units"], + cd_dtru_s: "drill through", + cd_dtru_l: ["additional drill depth below part bottom","overrides global z thru","0 to disable"], + cd_mark_s: "marking", + cd_mark_l: ["only mark holes, do not drill out","the step down setting is used","to determine how deep to mark"], + cd_ftop_s: "from stock top", + cd_ftop_l: ["drill from stock top","instead of part surface"], + cd_prcn_s: "precision", + cd_prcn_l: ["distance between slices","to search for holes","in workspace units","lower is slower and","uses more memory","0 = only flats"], + + cd_seli_l: ["individually select holes","to drill"], + cd_sela_s: "Select Matching Diameter", + cd_sela_l: ["select all holes that", "match selected tool diameter"], + + // CNC FLIP + cf_menu: "flip", + cf_nvrt_s: "z bottom", + cf_nvrt_l: "invert z bottom setting to follow stock", + + // CNC INDEX AXIS + ci_degr_s: "degrees", + ci_degr_l: "degrees to rotate the A (indexed) axis", + ci_abso_s: "absolute", + ci_abso_l: "degree rotation is absolute", + ci_face_s: "face", + ci_face_l: "select face to rotate facing up", + ci_line_s: "linear", + ci_line_l: "make linear passes along X then rotate Y", + + // CNC LASER On/Off Operations (Carvera) + cl_powr_s: "power", + cl_powr_l: "laser power from 0 to 1.0", + cl_adap_s: "adaptive", + cl_adap_l: "use adaptive power based on Z height", + cl_adrp_s: "repeating", + cl_adrp_l: "apply a moduluse of Z height using the given band to make repeating patterns", + cl_flat_s: "flatten", + cl_flat_l: "flatten output to a given Z height", + cl_flaz_s: "flat height", + cl_flaz_l: ["flatten output to this Z height", "in workspace units"], + cl_minp_s: "z min power", + cl_minp_l: ["power setting at the lowest part of the Z band. may be higher or lower than the other end of the Z band."], + cl_maxp_s: "z max power", + cl_maxp_l: ["power setting at the highest part of the Z band. may be higher or lower than the other end of the Z band."], + cl_minz_s: "min z height", + cl_minz_l: ["lowest Z band position"], + cl_maxz_s: "max z height", + cl_maxz_l: ["highest Z band position. when set to 0, uses the highest Z point of the workspace"], + + // CNC CUTOUT TABS + ct_menu: "tabs", + ct_angl_s: "angle", + ct_angl_l: ["starting angle for tab spacing","in degrees (0-360)"], + ct_numb_s: "count", + ct_numb_l: ["number of tabs to use","will be spaced evenly","around the part"], + ct_wdth_s: "width", + ct_wdth_l: "width in workspace units", + ct_hght_s: "height", + ct_hght_l: "height in workspace units", + ct_dpth_s: "depth", + ct_dpth_l: ["distance in workspace units","that the tab projects from","the part surface"], + ct_midl_s: "midline", + ct_midl_l: ["use midline of tab","instead of z bottom","for double-sided work"], + ct_nabl_s: "auto", + ct_nabl_l: ["auto generate radial tabs","projected from part center","using count and angle offset"], + + // OUTPUT + ou_menu: "output", + + // LASER SLICING + ls_offs_s: "offset / kerf", + ls_offs_l: ["distance between the center of the cutting","path and the edge of remaining material","in millimeters"], + ls_lahi_s: "slice height", + ls_lahi_l: ["layer height","in millimeters","0 = auto/detect"], + ls_lahm_s: "slice minimum", + ls_lahm_l: ["layer height minimum","will merge auto slices","under this thickness","in millimeters"], + ls_sngl_s: "single slice", + ls_sngl_l: ["perform only one slice","at specified layer height"], + + // LASER KNIFE + dk_menu: "drag knife", + dk_dpth_s: "cut depth", + dk_dpth_l: ["final cut depth","in millimeters"], + dk_pass_s: "cut passes", + dk_pass_l: ["number of passes","down to cut depth"], + dk_offs_s: "tip offset", + dk_offs_l: ["distance from blade tip","to center of tool","in millimeters"], + + // WIRE EDM + we_omit_s: "omit inner", + we_omit_l: "omit inner polygons", + + // LASER OUTPUT + ou_spac_s: "spacing", + ou_spac_l: ["distance between layer output","in millimeters"], + ou_scal_s: "scaling", + ou_scal_l: "multiplier (0.1 to 100)", + ou_powr_s: "power", + ou_powr_l: ["0 - 100","represents %"], + ou_sped_s: "speed", + ou_sped_l: "millimeters / second", + ou_mrgd_s: "merged", + ou_mrgd_l: ["merge all layers using","color coding to denote","stacking depth"], + ou_grpd_s: "grouped", + ou_grpd_l: ["retain each layer as","a unified grouping","instead of separated","polygons"], + ou_layr_s: "layer order", + ou_layr_l: ["output layer order","from top right to","bottom left"], + ou_layo_s: "layer color", + ou_layo_l: ["output layer colors","for each z index","overridden by merged"], + ou_lays_s: "layer marking", + ou_lays_l: ["mark layers for stacking. the layer above will be output in a different color which the laser can mark with a lower power. turns on layer grouping."], + ou_drkn_s: "drag knife", + ou_drkn_l: ["enable drag knife","output in gcode","cut radii are added","to corners with","cut down passes"], + ou_stak_s: "3d stack", + ou_stak_l: ["output is a 3D stack of 2D paths rather than packing layers flat in 2D"], + ou_maxp_s: "max power", + ou_maxp_l: ["max power value in gcode. power % from settings will scale from 0 to this value"], + ou_inch_s: "imperial", + ou_inch_l: ["output values in inches"], + ou_shap_s: "shaper tool", + ou_shap_l: ["annotate SVG with Shaper Tool custom attributes"], + + // OUTPUT FDM + ou_nozl_s: "nozzle temp", + ou_nozl_l: "degrees in celsius", + ou_bedd_s: "bed temp", + ou_bedd_l: "degrees in celsius", + ou_feed_s: "print speed", + ou_feed_l: ["max printing speed","set to 0 to use default","millimeters / second"], + ou_fini_s: "finish speed", + ou_fini_l: ["outermost shell speed","bridge/flat solid layers","set to 0 to use default","millimeters / second"], + ou_move_s: "move speed", + ou_move_l: ["non-print move speed","millimeters / second","0 = enable G0 moves"], + ou_shml_s: "shell factor", + ou_flml_s: "solid factor", + ou_spml_s: "infill factor", + ou_exml_l: ["extrusion multiplier","0.0 - 2.0"], + ou_fanl_s: "fan on layer", + ou_fanl_l: ["layer # to turn on fan","layers start at 0"], + ou_fans_s: "fan speed", + ou_fans_l: "0 - 255", + + // OUTPUT CAM + ou_toll_s: "precision", + ou_toll_l: ["surface precision","in workspace units","lower is slower and","uses more memory","0 = automatic based","on animate preference"], + ou_flat_s: "flatness", + ou_flat_l: ["delta z below which two adjacent points are considered co-linear. 0 = compute using tolerance"], + ou_redu_s: "reduction", + ou_redu_l: ["number of axis steps to skip for each unit of precision. higher precision may require reduction to produce reasonable file sizes. start at 0 and test in increments of 1."], + ou_brdg_s: "bridging", + ou_brdg_l: ["flat distance between curved","areas to bridge with a cut"], + ou_zanc_s: "z anchor", + ou_zanc_l: ["controls the position of the part","when stock Z exceeds part Z"], + ou_ztof_s: "z offset", + ou_ztof_l: ["offset z anchor","in workspace units"], + ou_ztop_s: "z top", + ou_ztop_l: ["offset from stock bottom","to set start of cutting depth","in workspace units","* drill/contour ignore this *"], + ou_zbot_s: "z bottom", + ou_zbot_l: ["offset from stock bottom","to limit cutting depth","in workspace units"], + ou_zclr_s: "z clearance", + ou_zclr_l: ["safe travel offset","from top of part","in workspace units"], + ou_ztru_s: "z thru", + ou_ztru_l: ["extend cutout pass down","in workspace units"], + ou_conv_s: "conventional", + ou_conv_l: ["milling direction","uncheck for 'climb'"], + ou_depf_s: "depth first", + ou_depf_l: ["optimize pocket cuts","with depth priority"], + ou_toin_s: "tool init", + ou_toin_l: "add tool change with first operation. when disabled, the gcode assumes the first tool is loaded and offset probed", + ou_forz_s: "force z max", + ou_forz_l: "all moves between pockets will go to z max (above stock). this is usually required if you are not performing a roughing operation before pocketing operations.", + ou_z1st_s: "first z max", + ou_z1st_l: "first move in the job will be to Z max (travel distance above stock)", + ou_feng_s: "engage factor", + ou_feng_l: "speed scale (0.1-1) when endmill is fully engaged on a new cut. stepped over cuts when the endmill is less than fully engaged run at a factor of 1 relative to output speed. typically this relates to roughing or any operation with step over.", + ou_eang_s: "ease angle", + ou_eang_l: "descent angle for ease down in degrees", + + // CAM STOCK + cs_menu: "stock", + cs_wdth_s: "width", + cs_wdth_l: ["width (x) in workspace units","0 defaults to part size"], + cs_dpth_s: "depth", + cs_dpth_l: ["depth (y) in workspace units","0 defaults to part size"], + cs_hght_s: "height", + cs_hght_l: ["height (z) in workspace units","0 defaults to part size"], + cs_offs_s: "offset", + cs_offs_l: ["use width, depth, height","as offsets from max","part size on platform"], + cs_clip_s: "clip to", + cs_clip_l: ["rough and outline","clip cutting paths","to defined stock"], + cs_ishg_s: "show grid", + cs_ishg_l: ["show platform grid in indexed mode"], + cs_indx_s: "indexed", + cs_indx_l: ["stock is mounted to a rotatary indexer"], + cs_offe_s: "enable", + cs_offe_l: "enable milling stock", + + // ORIGIN (CAM & LASER) + or_bnds_s: "origin bounds", + or_bnds_l: ["origin is relative to","boundary of all objects"], + or_cntr_s: "origin center", + or_cntr_l: "origin is referenced from the center", + or_topp_s: "origin top", + or_topp_l: "origin is references from the top of objects", + or_offx_s: "origin offset X", + or_offx_l: "origin offset X", + or_offy_s: "origin offset Y", + or_offy_l: "origin offset Y", + + // CAM ORIGIN + co_menu: "origin", + co_offx_s: "offset x", + co_offx_l: ["offset origin along x axis","in workspace units"], + co_offy_s: "offset y", + co_offt_l: ["offset origin along y axis","in workspace units"], + co_offz_s: "offset z", + co_offz_l: ["offset origin along z axis","in workspace units"], + + // FDM ADVANCED + ad_menu: "expert", + ad_rdst_s: "retract dist", + ad_rdst_l: ["amount to retract filament","for long moves. in millimeters"], + ad_rrat_s: "retract rate", + ad_rrat_l: ["speed of filament","retraction in mm/s"], + ad_rdwl_s: "engage dwell", + ad_wpln_s: "retract wipe", + ad_wpln_l: ["non-printing move","after retraction","in millimeters"], + ad_rdwl_l: ["time between re-engaging","filament and movement","in milliseconds"], + ad_scst_s: "shell coast", + ad_scst_l: ["non-printing end","of perimeter shells","in millimeters"], + ad_msol_s: "min solid", + ad_msol_l: ["minimum area (mm^2)","required to keep solid","must be > 0.1"], + ad_mins_s: "min speed", + ad_mins_l: ["minimum speed","for short segments"], + ad_spol_s: "short path", + ad_spol_l: ["polygons shorter than this","will have their print speed","scaled down to min speed","in millimeters"], + ad_arct_s: "arc tolerance", + ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","in millimeters","0 to disable"], + ad_zhop_s: "z hop dist", + ad_zhop_l: ["amount to raise z","on retraction moves","in millimeters","0 to disable"], + ad_abkl_s: "anti-backlash", + ad_abkl_l: ["for better flat surface finish","use micro-movements to cancel","backlash in solid layer output","in millimeters","0 to disable","if your firmware has M425","put that in the gcode header","and leave this as 0"], + ad_lbir_s: "belt first", + ad_lbir_l: ["always print shells touching the belt before any other shells. this is no longer a recommended setting based on extensive testing."], + ad_altr_s: "alternating", + ad_altr_l: ["alternate shell winding order","clockwise / counter-clockwise","may reduce warping in thin areas","and help with belt edge adhesion"], + ad_zint_s: "interleave z", + ad_zint_l: ["interleave Z heights with odd number shells"], + ad_lret_s: "layer retract", + ad_lret_l: ["force filament retraction","between layers"], + ad_agap_s: "avoid gaps", + ad_agap_l: ["route around gaps rather","than perform a retract","when possible"], + ad_play_s: "polish layers", + ad_play_l: ["polish up to specified","# of layers at a time"], + ad_pspd_s: "polish speed", + ad_pspd_l: ["polishing speed","in millimeters / second"], + ad_purg_s: "purge tower", + ad_purg_l: ["purge tower area in mm^2","for multi extruder setups","0 = disabled"], + + // CAM EXPERT + cx_fast_s: "skip shadow", + cx_fast_l: ["disable overhang detection","can be faster and use less","memory with complex models","but fails with overhangs","try enabling if slicing","hangs during shadowing"], + cx_true_s: "true shadow", + cx_true_l: ["computationally correct shadow","will be slower but produce","better cuts for complex parts"], + cx_arct_s: "arc tolerance", + cx_arct_l: ["convert cicrular paths to arcs;","center point drift tolerance","when matching arc points","consider values around 0.15","in mm, or 0.006 inches;","High values may cause","unexpected behavior","0 to disable"], + cx_arcr_s: "arc resolution", + cx_arcr_l: ["change in line direction","aceptible in an arc","consider values around 5","High values may cause","unexpected behavior",], + + // FDM GCODE + ag_menu: "gcode", + ag_nozl_s: "nozzle", + ag_nozl_l: "select output nozzle or head", + ag_peel_s: "peel guard", + ag_peel_l: ["starting at this belt z position","periodically roll the print off and","back on to the belt to unstick it","and prevent rolling deflection"], + // ag_paws_s: "pause layers", + // ag_paws_l: ["comma-separated list of layers","to inject pause commands before"], + // ag_loop_s: "loop layers", + // ag_loop_l: ["layer ranges to repeat in the format","first-last-count,first-last-count,...","omitted count = 1"], + ag_loop_s: "loop count", + ag_loop_l: ["number of times to repeat","the selected range or the","entire print if no range"], + + // SLA MENU + sa_menu: "slicing", + sa_lahe_s: "layer height", + sa_lahe_l: ["layer height","in millimeters"], + sa_shel_s: "hollow shell", + sa_shel_l: ["shell thickness in mm","use multiple of layer height","use 0 for solid (disabled)"], + sa_otop_s: "open top", + sa_otop_l: ["if shell is enabled","results in an open top"], + sa_obas_s: "open base", + sa_obas_l: ["if shell is enabled","results in an open base","disabled with supports"], + + sa_layr_m: "layers", + sa_lton_s: "light on time", + sa_lton_l: ["layer light on","time in seconds"], + sa_ltof_s: "light off time", + sa_ltof_l: ["layer light off","time in seconds"], + sa_pldi_s: "peel distance", + sa_pldi_l: ["peel distance","in millimeters"], + sa_pllr_s: "peel lift rate", + sa_pllr_l: ["peel lift speed","in mm/sec"], + sa_pldr_s: "peel drop rate", + sa_pldr_l: ["peel drop speed","in mm/sec"], + + sa_base_m: "base", + sa_balc_s: "layer count", + sa_balc_l: ["number of","base layers"], + sa_bltn_l: ["base layer light on","time in seconds"], + sa_bltf_l: ["base layer light off","time in seconds"], + + sa_infl_m: "infill", + sa_ifdn_s: "density", + sa_ifdn_l: ["percent infill","requires shell","0 = disabled","valid 0.0 - 1.0"], + sa_iflw_s: "line width", + sa_iflw_l: ["hatch line width","in millimeters"], + + sa_supp_m: "support", + sa_slyr_s: "base layers", + sa_slyr_l: ["base support layers","value range 0-10"], + sa_slgp_s: "gap layers", + sa_slgp_l: ["number of layers between","raft and bottom of object"], + sa_sldn_s: "density", + sa_sldn_l: ["used to compute the","number of support pillars","0.0-1.0 (0 = disable)"], + sa_slsz_s: "size", + sa_slsz_l: ["max size of a","support pillar","in millimeters"], + sa_slpt_s: "points", + sa_slpt_l: ["number of points in","each support pillar","in millimeters"], + sl_slen_l: "enable supports", + + sa_outp_m: "output", + sa_opzo_s: "z offset", + sa_opzo_l: ["z layer offset","almost always 0.0","0.0-1.0 in millimeters"], + sa_opaa_s: "anti alias", + sa_opaa_l: ["enable anti-aliasing","produces larger files","can blur details"] +}; diff --git a/web2/kiri/lang/es-es.js b/web2/kiri/lang/es-es.js new file mode 100644 index 00000000..944cf279 --- /dev/null +++ b/web2/kiri/lang/es-es.js @@ -0,0 +1,650 @@ +// Spanish localization for Kiri:Moto +// Will defer to English map for any missing key/value pairs +kiri.lang['es'] = +kiri.lang['es-es'] = { + // common keys and menus + animate: "animar", // CAM animate button + arrange: "organizar", // layout workspace objects + axis: "eje", // left object scale pop menu + clear: "limpiar", // clear workspace (remove all objects) + copy: "copiar", + delete: "eliminar", + detail: "detalle", + done: "hecho", + enable: "habilitar", + export: "exportar", + files: "archivos", + help: "ayuda", + ghost: "fantasma", // left render pop menu (wireframe) + hide: "ocultar", // left render pop menu (invisible) + home: "inicio", + import: "importar", + language: "idioma", + machine: "máquina", // device or machine + metric: "métrica", + name: "nombre", + prefs: "preferencias", // left menu "preferences" + preview: "vista previa", + recent: "reciente", + render: "renderizar", // left render pop menu + reset: "restablecer", + rotate: "rotar", // left rotate pop menu + save: "guardar", + scale: "escala", // left object scale pop menu + setup: "configurar", + settings: "configuración", + size: "tamaño", + slice: "corte", + solid: "sólido", // view type pop menu + start: "inicio", + tool: "herramienta", + tools: "herramientas", // CAM tool menu button + top: "arriba", + type: "tipo", // CAM tool type + version: "versión", + view: "vista", // left view pop menu + wire: "alambre", // left render pop menu + + acct_xpo: ["hace una copia de seguridad de su dispositivo","y perfiles de dispositivo con la ","opción de incluir espacio de trabajo","objetos y posiciones"], + + // RIGHT-CLICK CONTEXT MENU + rc_clws: "limpiar espacio de trabajo", + rc_xpws: "exportar espacio de trabajo", + rc_lafl: "apoyar sobre plano", + rc_mirr: "espejar", + rc_dupl: "duplicar", + rc_xstl: "exportar como STL", + + // DEVICE MENU and related dialogs + dm_sldt: "seleccionar un tipo de dispositivo", + dm_stdd: "dispositivos estándar", + dm_mydd: "mis dispositivos", + dm_seld: "dispositivo seleccionado", + dm_rcnt: "archivos recientes", + dm_savs: "ajustes guardados", + dm_appp: "Preferencias de la aplicación", + + // CAM Tool Dialog + td_tyem: "extremo", // end mill + td_tybm: "bola", // ball mill + td_tytm: "cono", // taper mill + td_tonm: "herramienta #", + td_shft: "asta", // endmill shaft specs + td_flut: "flauta", // endmill flute specs + td_tapr: "estrechar", // endmill taper specs + + // DEVICE dialog groups + dv_gr_dev: "dispositivo", + dv_gr_ext: "extrusora", + dv_gr_out: "salida", + dv_gr_gco: "macros de gcode", + + // DEVICE dialog (_s = label, _l = hover help) + dv_name_s: "nombre", + dv_name_l: "nombre del dispositivo", + dv_fila_s: "filamento", + dv_fila_l: "diámetro en milímetros", + dv_nozl_s: "boquilla", + dv_nozl_l: "diámetro en milímetros", + dv_bedw_s: "ancho", + dv_bedw_l: "unidades de espacio de trabajo", + dv_bedd_s: "profundidad", + dv_bedd_l: "unidades de espacio de trabajo", + dv_bedh_s: "altura", + dv_bedh_l: ["altura máxima de construcción","en unidades de espacio de trabajo"], + dv_spmx_s: "husillo máximo", + dv_spmx_l: ["velocidad máxima en rpm del husillo","0 para deshabilitar"], + dv_xtab_s: "posicionamiento absoluto", + dv_xtab_l: "extrusión se mueve en absoluto", + dv_orgc_s: "centro de origen", + dv_orgc_l: "centro de origen de la cama", + // dv_orgt_s: "parte superior del origen", + // dv_orgt_l: "parte superior z del origen de la parte", + dv_bedc_s: "cama circular", + dv_bedc_l: "la cama del dispositivo es circular", + dv_belt_s: "cama de cinta", + dv_belt_l: "cama de impresión continua", + dv_retr_s: "retracción de firmware", + dv_retr_l: ["el firmware del dispositivo es compatible con G10/G11"], + dv_fanp_s: "potencia del ventilador", + dv_fanp_l: "establecer la potencia del ventilador de refrigeración", + dv_prog_s: "progreso", + dv_prog_l: "salida en cada % de progreso", + dv_layr_s: "capa", + dv_layr_l: ["salida en cada","cambio de capa"], + dv_tksp_s: "espaciador de token", + dv_tksp_l: ["agregar un espacio entre","parámetros de eje de gcode","G0X0Y0X0","vs","G0 X0 Y0 Z0"], + dv_strc_s: "eliminar comentarios", + dv_strc_l: ["eliminar comentarios de gcode","los comentarios comienzan con ;"], + dv_fext_s: "extensión de archivo", + dv_fext_l: "extensión de nombre de archivo", + dv_dwll_s: "permanencia", + dv_dwll_l: "secuencia de comandos gcode de permanencia", + dv_tool_s: "cambio de herramienta", + dv_tool_l: "secuencia de comandos de cambio de herramienta", + dv_sspd_s: "velocidad del husillo", + dv_sspd_l: "establecer la velocidad del husillo", + dv_paus_s: "pausa", + dv_paus_l: "secuencia de comandos gcode de pausa ", + dv_head_s: "encabezado", + dv_head_l: "secuencia de comandos gcode de encabezado", + dv_foot_s: "pie", + dv_foot_l: "secuencia de comandos gcode de pie", + dv_lzon_s: "laser activado", + dv_lzon_l: "secuencia de comandos gcode para activar laser", + dv_lzof_s: "laser desactivado", + dv_lzof_l: "secuencia de comandos gcode para desactivar laser", + dv_exts_s: "", + dv_exts_l: "secuencia de comandos gcode para seleccionar este extrusor", + dv_dext_s: "deseleccionar", + dv_dext_l: "gcode ejecutado antes de habilitar otro extrusor", + dv_extd_s: "deseleccionar", + dv_extd_l: "secuencia de comandos gcode para deseleccionar este extrusor", + dv_exox_s: "compensación x x", + dv_exox_l: "compensación de boquilla x", + dv_exoy_s: "compensación x y", + dv_exoy_l: "compensación de boquilla y", + + // MODE + mo_menu: "modo", + mo_fdmp: "Impresión FDM", + mo_slap: "Impresión SLA", + mo_lazr: "Corte láser", + mo_cncm: "Fresadora CNC", + + // SETUP + su_menu: "configuración", + su_devi: "Dispositivos", + su_tool: "Herramientas", + su_locl: "Local", + su_xprt: "Exportar", + su_help: "Ayuda", + + // LOAD + fe_menu: "archivo", + fn_recn: "Reciente", + fn_impo: "Importar", + + // FUNCTION + fn_menu: "acción", + fn_arra: "Organizar", + fn_slic: "Cortar", + fn_prev: "Vista previa", + fn_expo: "Exportar", + + // VIEW + vu_menu: "ver", + vu_home: "Inicio", + vu_rset: "Restablecer", + vu_sptp: "Arriba", + vu_spfr: "Delantero", + vu_splt: "Izquierda", + vu_sprt: "Derecha", + + // WORKSPACE + ws_menu: "ver", + ws_save: "Guardar", + ws_cler: "Borrar", + + // OPTIONS + op_menu: "interfaz", + op_disp: "pantalla", + op_xprt_s: "experto", + op_xprt_l: "mostrar más opciones de configuración", + op_decl_s: "pegatinas", + op_decl_l: "mostrar pegatinas y logotipos del dispositivo", + op_dang_s: "experimental", + op_dang_l: "mostrar parámetros experimentales", + op_hopo_s: "menú flotante", + op_hopo_l: ["habilitar el menú flotante","activar"], + op_dark_s: "modo oscuro", + op_dark_l: "interfaz de modo oscuro", + op_comp_s: "interfaz de usuario compacta", + op_comp_l: ["interfaz de usuario compacta","mejor para pantallas pequeñas","y tabletas"], + op_shor_s: "mostrar origen", + op_shor_l: "mostrar origen del dispositivo o proceso", + op_shru_s: "mostrar reglas", + op_shru_l: ["mostrar reglas de ejes","en líneas de cuadrícula principales"], + op_sped_s: "mostrar velocidades", + op_sped_l: ["mostrar velocidad en la barra de colores","en modo de vista previa"], + op_auto_s: "distribución automática", + op_auto_l: ["plataforma de distribución automática","cuando se agregan nuevos elementos"], + op_free_s: "distribución libre", + op_free_l: ["permitir distribución arrastrable","sin efecto en modo láser"], + op_spcr_s: "espaciado", + op_spcr_l: ["espaciado entre objetos","durante la distribución automática","en unidades del espacio de trabajo"], + op_orth_s: "ortográfica", + op_orth_l: ["visualización ortográfica","requiere actualización de página"], + op_invr_s: "invertir zoom", + op_invr_l: ["invertir rueda del mouse","zoom de desplazamiento"], + op_save_s: "guardado automático", + op_save_l: ["preservar objetos en el espacio de trabajo","entre recargas de aplicaciones"], + op_line_s: "tipo de línea", + op_line_l: ["tipo de línea para renderizado de caminos","impacta el desempeño 3D","camino: mejor para 3D","plano: bueno para 2D","línea = rápido para 1D"], + op_unit_s: "unidades", + op_unit_l: ["las unidades del espacio de trabajo afectan","velocidades y distancias"], + op_anim_s: "animar", + op_anim_l: ["densidad de malla de animación","mayor valor es más denso","ocupa más memoria","y es más lento"], + + lo_menu: "distribución", + + pt_menu: "partes", + pt_deci_s: "diezmar", + pt_deci_l: ["habilitar o deshabilitar el diezmado de puntos","durante la importación del puerto para un corte más rápido","y un menor uso de memoria"], + pt_qual_s: "calidad", + pt_qual_l: ["nivel de detalle a retener","durante las operaciones de corte","más bajo es más rápido"], + pt_heal_s: "curar malla", + pt_heal_l: ["intentar curar","mallas no topológicas","extiende el tiempo de corte"], + + xp_menu: "exportaciones", + + // SETTINGS + se_menu: "perfil", + se_load: "cargar", + se_save: "guardar", + + // FDM SLICING + sl_menu: "capas", + sl_lahi_s: "altura", + sl_lahi_l: ["altura de cada sección","capa en milímetros"], + ad_minl_s: "altura mínima", + ad_minl_l: ["altura mínima adaptativa de la capa","en milímetros","debe ser distinta de cero"], + sl_ltop_s: "capas superiores", + sl_ltop_l: ["cantidad de capas sólidas","para hacer cumplir en la","parte superior de la impresión"], + sl_lsld_s: "capas sólidas", + sl_lsld_l: ["áreas de relleno sólido calculadas","de los deltas de capa. Ver","menú flotante de capa"], + sl_lbot_s: "capas base", + sl_lbot_l: ["cantidad de capas sólidas","para hacer cumplir en la","parte inferior de la impresión"], + ad_adap_s: "adaptable", + ad_adap_l: ["usar alturas de capa adaptables","con 'altura de capa' como máximo","y 'min capa' como mínimo"], + + // FDM SHELLS + sl_shel_s: "cantidad de cáscaras", + sl_shel_l: ["cantidad de muros","perimetrales a generar"], + sl_ordr_s: "orden de cáscaras", + sl_ordr_l: ["orden de cáscaras de salida","de adentro hacia afuera","o de afuera hacia adentro","afecta la calidad de la superficie"], + sl_strt_s: "inicio de la capa", + sl_strt_l: ["punto de inicio de la capa","última = último final de capa","centro = centro de la parte","origen = origen del dispositivo"], + ad_thin_s: "paredes delgadas", + ad_thin_l: ["detectar y rellenar huecos","entre las paredes de la carcasa"], + + // FDM FILL + fi_menu: "relleno", + fi_type: "tipo de relleno", + fi_pcnt_s: "fracción de relleno", + fi_pcnt_l: ["valores de densidad de relleno","0.0 - 1.0"], + fi_angl_s: "inicio sólido", + fi_angl_l: ["ángulo de inicio en grados","90 grados añadidos a ","cada capa siguiente ","se aplica solo a capas sólidas"], + fi_wdth_s: "ancho sólido", + fi_wdth_l: ["ancho de línea para relleno sólido","como una fracción del ancho de la boquilla","los valores < 1 son más densos","y buenos para acabados superficiales","0.0 - 1.0"], + fi_over_s: "superposición de cáscara", + fi_over_l: ["superposición con la cáscara y otro relleno","como fracción del diámetro de la boquilla","0.0 - 2.0"], + // fi_rate_s: "velocidad de impresión", + fi_rate_l: ["velocidad de extrusión del relleno","establecido en 0 para utilizar las velocidades","de salida de impresión predeterminadas"], + + // FDM FIRST LAYER + fl_menu: "base", + fl_lahi_s: "altura de capa", + fl_lahi_l: ["altura de cada rebanada","en milímetros","debe ser >= altura de rebanada"], + fl_rate_s: "velocidad de la cáscara", + fl_rate_l: ["velocidad máxima de impresión de la cáscara","en milímetros/segundo"], + fl_frat_s: "velocidad de llenado", + fl_frat_l: ["velocidad máxima de impresión de llenado","en milímetros / segundo"], + fl_mult_s: "factor de flujo", + fl_mult_l: ["multiplicador de extrusión","0.0 - 2.0"], + fl_sfac_s: "factor de ancho", + fl_sfac_l: ["multiplicador del tamaño de la boquilla","cambia el espaciado de línea"], + fl_skrt_s: "cantidad de faldones", + fl_skrt_l: ["número de desfase de la primera capa","bordes a generar"], + fl_skro_s: "desfase de faldón", + fl_skro_l: ["desfase de faldón de la pieza","en milímetros"], + fl_nozl_s: "temperatura de la boquilla", + fl_nozl_l: ["en grados centígrados","configuración de salida utilizada","cuando es cero"], + fl_bedd_s: "temperatura de la cama", + fl_bedd_l: ["en grados centígrados","configuración de salida utilizada","cuando es cero"], + fr_spac_s: "espacio de la balsa", + fr_spac_l: ["espaciado de capa adicional","entre la 1ra capa y la balsa","en milímetros"], + fr_nabl_s: "habilitar de balsa", + fr_nabl_l: ["crear una balsa debajo del","modelo para una mejor adherencia","utiliza desplazamiento de faldón y","inhabilita salida de falda"], + + // FDM BELT ONLY + fl_zoff_s: "compensación de la banda", + fl_zoff_l: ["compensación de la altura de la banda","de la extrusión más baja","en milímetros"], + fl_brim_s: "tamaño del borde", + fl_brim_l: ["agregar borde a la parte inferior","el tamaño es el ancho en milímetros","0 para deshabilitar"], + fl_brmn_s: "gatillo de borde", + fl_brmn_l: ["agregar bordes solo cuando el segmento","que da a la cinta sea más corto que este","valor en milímetros","0 = Infinito"], + fl_bled_s: "anclaje parcial", + fl_bled_l: ["anclaje parcial del cinturón","al inicio de la impresión","en milímetros"], + + // FDM SUPPORT + sp_menu: "soporte", + sp_detect: "detectar", + sp_dens_s: "densidad", + sp_dens_l: ["fracción 0.0 - 1.0","recomendado 0.15","0 para deshabilitar"], + sp_size_s: "tamaño del pilar", + sp_size_l: ["ancho del pilar","en milímetros"], + sp_offs_s: "desplazamiento de la pieza", + sp_offs_l: ["desplazamiento desde la pieza","en milímetros"], + sp_gaps_s: "capas de separación", + sp_gaps_l: ["cantidad de capas","desplazamiento desde la parte"], + sp_span_s: "intervalo máximo", + sp_span_l: ["intervalo no admitido que provoca","la generación de un nuevo soporte","en milímetros"], + sp_angl_s: "ángulo máximo", + sp_angl_l: ["ángulo máximo de voladizo antes","de generar un pilar de soporte"], + sp_area_s: "área mínima", + sp_area_l: ["área mínima para","una columna de soporte","en milímetros"], + sp_xpnd_s: "expandir", + sp_xpnd_l: ["expandir el área de soporte","más allá del límite de la pieza","en milímetros"], + sp_nozl_s: "extrusora", + sp_nozl_l: ["en sistemas de múltiples extrusoras","la extrusora que se utilizará para","material de soporte"], + sp_auto_s: "automático", + sp_auto_l: ["habilitar soportes generados","usando geometría en tiempo de corte","los soportes solo aparecerán","después de que se complete el corte"], + + // LASER SLICING + ls_offs_s: "desplazamiento", + ls_offs_l: ["ajustar para ancho de viga","en milímetros"], + ls_lahi_s: "altura", + ls_lahi_l: ["altura de capa","en milímetros","0 = auto/detectar"], + ls_lahm_s: "mínimo", + ls_lahm_l: ["altura de capa mínima","fusionará cortes automáticos","de menos de este grosor","en milímetros"], + ls_sngl_s: "único", + ls_sngl_l: ["realizará solo un corte","a la altura de capa especificada"], + + // CNC COMMON terms + cc_tool: "herramienta", + cc_offs_s: "desplazamiento", + cc_offs_l: ["desplazamiento del centro de la herramienta","desde la ruta elegida"], + cc_spnd_s: "rpm del husillo", + cc_spnd_l: ["velocidad del husillo en","revoluciones/minuto"], + cc_sovr_s: "paso por encima", + cc_sovr_l: ["como una fracción del","diámetro de la herramienta"], + cc_sdwn_s: "reducir paso", + cc_sdwn_l: ["reducir la profundidad de paso","para cada pasada","en unidades de espacio de trabajo","0 para deshabilitar"], + cc_feed_s: "velocidad de avance", + cc_feed_l: ["velocidad máxima de corte en","unidades de espacio de trabajo / minuto"], + cc_plng_s: "tasa de inmersión", + cc_plng_l: ["velocidad máxima del eje z en","unidades de espacio de trabajo / minuto"], + cc_sngl_s: "seleccionar solo líneas", + cc_sngl_l: ["seleccionar solo bordes individuales","en lugar de polilíneas conectadas"], + + // CNC COMMON + cc_menu: "límites", + cc_flip: "voltear", + cc_rapd_s: "avance xy", + cc_rapd_l: ["velocidad máxima de movimientos xy","en unidades de espacio de trabajo / minuto"], + cc_rzpd_s: "avance z", + cc_rzpd_l: ["velocidad máxima de movimientos z","en unidades de espacio de trabajo / minuto"], + + cc_loff_s: "desplazamiento", + cc_loff_l: ["distancia desde la superficie del material","para pasada de nivelación","en unidades de espacio de trabajo"], + + // CNC ROUGHING + cr_menu: "desbaste", + cr_lsto_s: "dejar material", + cr_lsto_l: ["desplazamiento horizontal de las caras verticale","material para dejar para la pasada de acabado","en unidades de espacio de trabajo"], + cr_ease_s: "ease down", + cr_ease_l: ["cortes de inmersión","bajarán en espiral o suavizarán","siguiendo una ruta lineal"], + cr_clrt_s: "despejar la parte superior", + cr_clrt_l: ["ejecutar una pasada de despeje","por el área delimitante de la pieza","en z = 0"], + cr_clrp_s: "despejar huecos", + cr_clrp_l: ["fresar a través de cavidades","en lugar de solo el contorno"], + cr_clrf_s: "limpiar caras", + cr_clrf_l: ["interpolar el paso hacia abajo para","limpiar las áreas planas detectadas"], + cr_olin_s: "solo en el interior", + cr_olin_l: ["limitar el corte a","los límites internos de la pieza"], + + // CNC OUTLINE + co_menu: "contorno", + co_dogb_s: "dogbones", + co_dogb_l: ["insertar cortes de hueso de perro","en las esquinas interiores"], + co_wide_s: "recorte ancho", + co_wide_l: ["ensanchar las rutas de corte exteriores","para cortes profundos en material duro"], + co_olin_s: "solo en el interior", + co_olin_l: ["limitar el corte a","límites de la parte interior"], + co_olot_s: "sólo exterior", + co_olot_l: ["limitar el corte a","límites de la parte exterior","que se pueden considerar","como el contorno de la sombra"], + co_omit_s: "omitir a través", + co_omit_l: "eliminar agujeros pasantes", + co_olen_s: "habilitar", + co_olen_l: "corte de contorno habilitado", + + // CNC CONTOUR + cn_menu: "contorno", + cf_angl_s: "ángulo máximo", + cf_angl_l: ["ángulos mayores que este","se consideran verticales"], + cf_curv_s: "solo curvas", + cf_curv_l: ["limitar la limpieza lineal","a superficies curvas"], + cf_olin_s: "solo interior", + cf_olin_l: ["limitar el corte a","límites de la parte interior"], + cf_linx_s: "habilitar paso y", + cf_linx_l: "acabado lineal del eje y", + cf_liny_s: "habilitar paso x", + cf_liny_l: "acabado lineal del eje x", + + // CNC TRACE + cu_menu: "traza", + cu_type_s: "tipo", + cu_type_l: ["seguir = la punta de la herramienta sigue la línea","derecha o izquierda = punta de la herramienta","sigue el desplazamiento de la línea por el radio de la herramienta"], + + // CNC DRILLING + cd_menu: "taladrar", + cd_axis: "eje", + cd_points: "puntos", + cd_plpr_s: "inmersión por", + cd_plpr_l: ["inmersión máxima entre","períodos de permanencia","en unidades de espacio de trabajo","0 para deshabilitar"], + cd_dwll_s: "tiempo de permanencia", + cd_dwll_l: ["tiempo de permanencia","entre inmersiones en","en milisegundos"], + cd_lift_s: "elevación de perforación", + cd_lift_l: ["elevación entre inmersiones","después del período de permanencia","en unidades de espacio de trabajo","0 para deshabilitar"], + cd_regi_s: "registro", + cd_regi_l: ["agujeros de registro de perforación","para piezas de doble cara","independiente de la habilitación","del taladrado pero utiliza la misma","herramienta y ajustes"], + + // CNC CUTOUT TABS + ct_menu: "pestañas", + ct_angl_s: "ángulo", + ct_angl_l: ["ángulo inicial para el espaciado de pestañas","en grados (0-360)"], + ct_numb_s: "cantidad", + ct_numb_l: ["cantidad de pestañas a utilizar","se espaciarán uniformemente","alrededor de la pieza"], + ct_wdth_s: "ancho", + ct_wdth_l: "ancho en unidades de espacio de trabajo", + ct_hght_s: "altura", + ct_hght_l: "altura en unidades de espacio de trabajo", + ct_dpth_s: "profundidad", + ct_dpth_l: ["distancia en unidades de espacio de trabajo","desde la que se proyecta la pestaña","en la superficie de la pieza"], + ct_midl_s: "línea media", + ct_midl_l: ["usar línea media de la pestaña","en lugar de fondo z","para trabajo a doble cara"], + ct_nabl_s: "auto", + ct_nabl_l: ["autogenerar pestañas radiales","proyectadas desde el centro de la pieza","usando recuento y desplazamiento de ángulo"], + + // OUTPUT + ou_menu: "salida", + + // LASER KNIFE + dk_menu: "cuchillo", + dk_dpth_s: "profundidad de corte", + dk_dpth_l: ["profundidad de corte final","en milímetros"], + dk_pass_s: "pasadas de corte", + dk_pass_l: ["cantidad de pasadas","hasta la profundidad de corte"], + dk_offs_s: "desplazamiento de la punta", + dk_offs_l: ["distancia desde la punta de la hoja","al centro de la herramienta","en milímetros"], + + // OUTPUT LASER + ou_spac_s: "espaciado", + ou_spac_l: ["distancia entre la salida de la capa","en milímetros"], + ou_scal_s: "escala", + ou_scal_l: "multiplicador (0,1 a 100)", + ou_powr_s: "potencia", + ou_powr_l: ["0 - 100","representa %"], + ou_sped_s: "velocidad", + ou_sped_l: "milímetros / segundo", + ou_mrgd_s: "fusionadas", + ou_mrgd_l: ["fusionar todas las capas usando","codificación de colores para indicar","la profundidad de apilamiento"], + ou_grpd_s: "agrupadas", + ou_grpd_l: ["conservar cada capa como","una agrupación unificada","en lugar de polígonos","separados"], + ou_layr_s: "orden de las capas", + ou_layr_l: ["oorden de las capas de salida","de arriba a la derecha a","abajo a la izquierda"], + ou_layo_s: "color de la capa", + ou_layo_l: ["colores de las capas de salida","para cada índice z","anulado por fusionado"], + ou_drkn_s: "cuchillo de arrastre", + ou_drkn_l: ["habilitar cuchillo de arrastre","salida en gcode","se agregan radios de corte","a las esquinas con","pasadas de corte"], + + // OUTPUT FDM + ou_nozl_s: "temperatura de la boquilla", + ou_nozl_l: "en grados centígrados", + ou_bedd_s: "temperatura de la cama", + ou_bedd_l: "en grados centígrados", + ou_feed_s: "velocidad de impresión", + ou_feed_l: ["velocidad máxima de impresión","milímetros / segundo"], + ou_fini_s: "velocidad de impresión", + ou_fini_l: ["velocidad de la capa más externad","milímetros / segundo"], + ou_move_s: "velocidad de movimiento", + ou_move_l: ["velocidad de movimiento sin impresión","milímetros / segundo","0 = habilitar movimientos G0"], + ou_shml_s: "factor de cáscara", + ou_flml_s: "factor sólido", + ou_spml_s: "factor de relleno", + ou_exml_l: ["multiplicador de extrusión","0.0 - 2.0"], + ou_fans_s: "velocidad del ventilador", + ou_fans_l: "0 - 255", + + // OUTPUT CAM + ou_toll_s: "tolerancia", + ou_toll_l: ["precisión de superficie","en unidades de espacio de trabajo","menor es más lento y","usa más memoria","0 = basado en automático","en preferencia animada"], + ou_zanc_s: "ancla z", + ou_zanc_l: ["controla el posición de la pieza","cuando el material Z excede la parte Z"], + ou_ztof_s: "desplazamiento z", + ou_ztof_l: ["desplazar el ancla z","en unidades de espacio de trabajo","no tiene ningún efecto cuando","el ancla está en el medio"], + ou_zbot_s: "z inferior", + ou_zbot_l: ["desplazamiento desde la parte inferior","para limitar la profundidad de corte","en unidades de espacio de trabajo"], + ou_zclr_s: "espacio libre z", + ou_zclr_l: ["desplazamiento seguro del recorrido","desde la parte superior de la pieza","en unidades de espacio de trabajo"], + ou_ztru_s: "a través de z", + ou_ztru_l: ["extender pasada de corte hacia abajo","en unidades de espacio de trabajo"], + ou_conv_s: "convencional", + ou_conv_l: ["dirección de fresado","desmarcar para 'subir'"], + ou_depf_s: "profundidad primero", + ou_depf_l: ["optimizar cortes empotrados","con prioridad de profundidad"], + + // CAM STOCK + cs_menu: "material", + cs_wdth_s: "ancho", + cs_wdth_l: ["ancho (x) en unidades del espacio de trabajo","0 usa el tamaño de la pieza"], + cs_dpth_s: "profundidad", + cs_dpth_l: ["profundidad (y) en unidades de espacio de trabajo","0 usa el tamaño de la pieza"], + cs_hght_s: "altura", + cs_hght_l: ["altura (z) en unidades de espacio de trabajo","0 usa el tamaño de la pieza"], + cs_offs_s: "desplazamiento", + cs_offs_l: ["usar ancho, profundidad, altura","como desplazamientos del tamaño","máximo de pieza en la plataforma"], + cs_clip_s: "recortar a", + cs_clip_l: ["desbaste y delinear","recortar rutas de corte","al material definido"], + cs_offe_s: "habilitar", + cs_offe_l: "habilitar material de fresado", + + // ORIGIN (CAM & LASER) + or_bnds_s: "límites de origen", + or_bnds_l: ["origen relativo al","límite de todos los objetos"], + or_cntr_s: "centro de origen", + or_cntr_l: "origen referenciado desde el centro", + or_topp_s: "origen superior", + or_topp_l: "origen es referenciado desde la parte superior de los objetos", + + // FDM ADVANCED + ad_menu: "experto", + ad_rdst_s: "distancia de retracción", + ad_rdst_l: ["cantidad de filamento a retraer","para movimientos largos. en milímetros"], + ad_rrat_s: "velocidad de retracción", + ad_rrat_l: ["velocidad del filamento","retracción en mm/s"], + ad_rdwl_s: "activar el reposo", + ad_wpln_s: "retracción de barrido", + ad_wpln_l: ["movimiento sin impresión","después de la retracció","en milímetros"], + ad_rdwl_l: ["tiempo hasta volver a activar","filamento y movimiento","en milisegundos"], + ad_scst_s: "costa de la cáscara", + ad_scst_l: ["extremo no imprimible","de las cáscaras perimetrales","en milímetros"], + ad_msol_s: "mínimo sólido", + ad_msol_l: ["área mínima (en mm^2)","requerida para mantenerse sólido","debe ser > 0.1"], + ad_mins_s: "velocidad mínima", + ad_mins_l: ["velocidad mínima","para segmentos cortos"], + ad_spol_s: "ruta corta", + ad_spol_l: ["los polígonos más cortos que este","tendrán su velocidad de impresión","reducida a velocidad mínima","en milímetros"], + ad_arct_s: "tolerancia de arco", + ad_arct_l: ["convertir líneas facetadas en arcos","tolerancia de deriva del punto central","al hacer coincidir puntos de arco","considerar valores alrededor de 0.15","en milímetros","0 para deshabilitar"], + ad_zhop_s: "distancia z de salto", + ad_zhop_l: ["cantidad de elevación z","en movimientos de retracción","en milímetros","0 para deshabilitar"], + ad_abkl_s: "anti-retroceso", + ad_abkl_l: ["para un mejor acabado de superficies planas","usa micro-movimientos para cancelar","el retroceso en salida de capas planas","en milímetros","0 para deshabilitar","si su firmware tiene M425","poner eso en el encabezado de gcode","y dejar este valor 0"], + ad_lret_s: "retraer la capa", + ad_lret_l: ["forzar la retracción del filamento","entre capas"], + ad_play_s: "pulir capas", + ad_play_l: ["pulir hasta el nivel especificado","número de capas a la vez"], + ad_pspd_s: "velocidad de pulido", + ad_pspd_l: ["velocidad de pulido","en milímetros / minuto"], + + // CAM EXPERT + cx_fast_s: "omitir sombra", + cx_fast_l: ["deshabilitar la detección de voladizos","puede ser más rápido y usar menos","memoria en modelos complejos","pero falla con voladizos","intente habilitar si al hacer cortes","se cuelga durante el sombreado"], + + // FDM GCODE + ag_menu: "gcode", + ag_nozl_s: "boquilla", + ag_nozl_l: "seleccionar boquilla o cabezal de salida", + ag_peel_s: "protector de desprendimiento", + ag_peel_l: ["comenzando en esta posición z de la cinta","enrollar periódicamente la impresión, y","retroceder la cinta para despegarla","y evitar la desviación rodante"], + ag_paws_s: "capas de pausa", + ag_paws_l: ["lista de capas separadas por comas","antes de las cuales inyectar comandos de pausa"], + ag_loop_s: "capas de bucle", + ag_loop_l: ["rangos de capas para repetir en el formato","primera-última-cuenta, primera-última-cuenta,...","conteo omitido = 1"], + + // SLA MENU + sa_menu: "corte", + sa_lahe_s: "altura de capa", + sa_lahe_l: ["altura de capa","en milímetros"], + sa_shel_s: "cáscara hueca", + sa_shel_l: ["espesor de capa en mm","usar múltiplos de altura de capa","usar 0 para sólido (deshabilitado)"], + sa_otop_s: "parte superior abierta", + sa_otop_l: ["si la cáscara está habilitada","da como resultado una parte superior abierta"], + sa_obas_s: "base abierta", + sa_obas_l: ["si la cáscara está habilitada","da como resultado una base abierta","deshabilitado si se usan soportes"], + + sa_layr_m: "capas", + sa_lton_s: "luz de tiempo", + sa_lton_l: ["luz de capa activada","tiempo en segundos"], + sa_ltof_s: "luz de tiempo desactivada", + sa_ltof_l: ["luz de capa desactivada","tiempo en segundos"], + sa_pldi_s: "distancia de pelado", + sa_pldi_l: ["distancia de pelado","en milímetros"], + sa_pllr_s: "tasa de elevación de pelado", + sa_pllr_l: ["velocidad de elevación de pelado","en mm/sec"], + sa_pldr_s: "tasa de caída de pelado", + sa_pldr_l: ["velocidad de caída de pelado","en mm/sec"], + + sa_base_m: "base", + sa_balc_s: "recuento de capas", + sa_balc_l: ["cantidad de","capas base"], + sa_bltn_l: ["luz de la capa base activada","tiempo en segundos"], + sa_bltf_l: ["luz de la capa base desactivada","tiempo en segundos"], + + sa_infl_m: "relleno", + sa_ifdn_s: "densidad", + sa_ifdn_l: ["porcentaje de relleno","requiere cáscara","0 = deshabilitado","válido 0.0 - 1.0"], + sa_iflw_s: "ancho de línea", + sa_iflw_l: ["ancho de línea de trama","en milímetros"], + + sa_supp_m: "soporte", + sa_slyr_s: "capas base", + sa_slyr_l: ["capas base de soporte","rango de valores 0-10"], + sa_slgp_s: "capas de separación", + sa_slgp_l: ["cantidad de capas entre","la balsa y fondo del objeto"], + sa_sldn_s: "densidad", + sa_sldn_l: ["utilizada para calcular la","cantidad de pilares de soporte","0.0-1.0 (0 = deshabilitar)"], + sa_slsz_s: "tamaño", + sa_slsz_l: ["tamaño máximo de un","pilar de soporte","en milímetros"], + sa_slpt_s: "puntos", + sa_slpt_l: ["cantidad de puntos en","cada pilar de soporte","en milímetros"], + sl_slen_l: "habilitar soportes", + + sa_outp_m: "salida", + sa_opzo_s: "desplazamiento z", + sa_opzo_l: ["desplazamiento de capa z","casi siempre es 0.0","0.0-1.0 en milímetros"], + sa_opaa_s: "suavizado", + sa_opaa_l: ["acctivar suavizado","produce archivos más grandes","puede eliminar detalles"] +}; diff --git a/web2/kiri/lang/es.js b/web2/kiri/lang/es.js new file mode 120000 index 00000000..95f517ab --- /dev/null +++ b/web2/kiri/lang/es.js @@ -0,0 +1 @@ +es-es.js \ No newline at end of file diff --git a/web2/kiri/lang/fr-fr.js b/web2/kiri/lang/fr-fr.js new file mode 120000 index 00000000..b0fd9cf9 --- /dev/null +++ b/web2/kiri/lang/fr-fr.js @@ -0,0 +1 @@ +fr.js \ No newline at end of file diff --git a/web2/kiri/lang/fr.js b/web2/kiri/lang/fr.js new file mode 100644 index 00000000..e30df48f --- /dev/null +++ b/web2/kiri/lang/fr.js @@ -0,0 +1,642 @@ +// French localization for Kiri:Moto +// Will defer to English map for any missing key/value pairs +kiri.lang['fr'] = +kiri.lang['fr-fr'] = { + // common keys and menus + animate: "animer", // CAM animate button + arrange: "organiser", // layout workspace objects + axis: "axe", // left object scale pop menu + clear: "effacer", // clear workspace (remove all objects) + copy: "copier", + delete: "supprimer", + detail: "détail", + done: "terminé", + enable: "permettre", + export: "exporter", + files: "fichiers", + help: "aide", + ghost: "translucide", // left render pop menu (wireframe) + hide: "cacher", // left render pop menu (invisible) + home: "position d'origine", + import: "importer", + language: "langue", + machine: "machine", // device or machine + metric: "métrique", + name: "nom", + prefs: "préfs", // left menu "preferences" + preview: "prévisualiser", + recent: "récent", + render: "rendu", // left render pop menu + reset: "remettre", + rotate: "tourner", // left rotate pop menu + save: "sauvegarder", + scale: "échelle", // left object scale pop menu + setup: "setup", + settings: "paramètres", + size: "taille", + solid: "solide", // view type pop menu + tool: "outil", + tools: "outils", // CAM tool menu button + top: "haut", + type: "genre", // CAM tool type + version: "version", + view: "vue", // left view pop menu + wire: "filaire", // left render pop menu + + acct_xpo: ["faites une sauvegarde de votre machine","et des profils des machines avec","l'option d'inclure les","objets et positions d'environnement"], + + // RIGHT-CLICK CONTEXT MENU + rc_clws: "effacer l'environnement", + rc_xpws: "exporter l'environnement", + rc_lafl: "poser à plat", + rc_mirr: "refléter", + rc_dupl: "dupliquer", + rc_xstl: "exporter en STL", + + // DEVICE MENU and related dialogs + dm_sldt: "sélectionner un genre de machine", + dm_stdd: "machines standards", + dm_mydd: "mes machines", + dm_seld: "machine sélectionnée", + dm_rcnt: "fichiers récents", + dm_savs: "paramètres sauvegardés", + dm_appp: "Préférences d'Application", + + // CAM Tool Dialog + td_tyem: "plat", // end mill + td_tybm: "boule", // ball mill + td_tytm: "v forme", // taper mill + td_tonm: "outil #", + td_shft: "tige", // endmill shaft specs + td_flut: "flûte", // endmill flute specs + td_tapr: "cône", // endmill taper specs + + // DEVICE dialog groups + dv_gr_dev: "périphérique", + dv_gr_ext: "extrudeur", + dv_gr_out: "sortie", + dv_gr_gco: "macros gcode", + + // DEVICE dialog (_s = label, _l = hover help) + dv_name_s: "nom", + dv_name_l: "nom du matériel", + dv_fila_s: "filament", + dv_fila_l: "diamètre en millimètres", + dv_nozl_s: "buse", + dv_nozl_l: "diamètre en millimètres", + dv_bedw_s: "largeur", + dv_bedw_l: "unités de l'espace de travail", + dv_bedd_s: "profondeur", + dv_bedd_l: "unités de l'espace de travail", + dv_bedh_s: "hauteur", + dv_bedh_l: ["hauteur de construction maximale","en unités de l'espace de travail"], + dv_spmx_s: "max spindle", + dv_spmx_l: ["max spindle rpm speed","0 to disable"], + dv_xtab_s: "positionnement absolu", + dv_xtab_l: "déplacements extrusion absolu", + dv_orgc_s: "origine au centre", + dv_orgc_l: "origin du plateau au centre", + // dv_orgt_s: "origin top", + // dv_orgt_l: "part z origin top", + dv_bedc_s: "plateau circulaire", + dv_bedc_l: "le plateau du matériel est circulaire", + dv_belt_s: "plateau tapis-roulant", + dv_belt_l: "plateau à impression continue", + dv_retr_s: "rétraction firmware", + dv_retr_l: ["le firmware supporte G10/G11"], + dv_fanp_s: "refroidissement", + dv_fanp_l: "régler la puissance du ventilateur de refroidissement", + dv_prog_s: "progression", + dv_prog_l: "résultats de chaque % de progression", + dv_layr_s: "couche", + dv_layr_l: ["résultats de chaque","changement de couche"], + dv_tksp_s: "délimiteur d'instruction", + dv_tksp_l: ["ajouter un espace","entre les paramètres des axes","G0X0Y0X0","vs","G0 X0 Y0 Z0"], + dv_strc_s: "éliminer les commentaires", + dv_strc_l: ["éliminer les commentaires du gcode","les commentaires débutent par ;"], + dv_fext_s: "extension du fichier", + dv_fext_l: "extension du nom de fichier", + dv_dwll_s: "dwell", + dv_dwll_l: "script gcode dwell", + dv_tool_s: "changement d'outil", + dv_tool_l: "script de changement d'outil", + dv_sspd_s: "spindle speed", + dv_sspd_l: "set spindle speed", + dv_paus_s: "pause", + dv_paus_l: "script gcode de pause", + dv_head_s: "header", + dv_head_l: "script gcode de démarrage", + dv_foot_s: "footer", + dv_foot_l: "script gcode de fin", + dv_lzon_s: "laser on", + dv_lzon_l: "gcode laser on script", + dv_lzof_s: "laser off", + dv_lzof_l: "gcode laser off script", + dv_exts_s: "sélectionner", + dv_exts_l: "gcode pour activer cet extrudeur", + dv_dext_s: "désélectionner", + dv_dext_l: "gcode à lancer avant d'activer un autre extrudeur", + dv_extd_s: "désélectionner", + dv_extd_l: "gcode pour désactiver cet extrudeur", + dv_exox_s: "décalage x", + dv_exox_l: "décalage de buse en x", + dv_exoy_s: "décalage y", + dv_exoy_l: "décalage de buse en y", + + // MODE + mo_menu: "mode", + mo_fdmp: "Impression FDM", + mo_slap: "Impression SLA", + mo_lazr: "Découpe / gravure Laser", + mo_cncm: "Routeur CNC", + + // SETUP + su_menu: "installation", + su_devi: "Matériels", + su_tool: "Outils", + su_locl: "Local", + su_xprt: "Exporter", + su_help: "Aide", + + // LOAD + fe_menu: "fichier", + fn_recn: "Récents", + fn_impo: "Importer", + + // FUNCTION + fn_menu: "action", + fn_arra: "Organiser", + fn_slic: "Découper", + fn_prev: "Prévisualiser", + fn_expo: "Exporter", + + // VIEW + vu_menu: "vue", + vu_home: "Origine", + vu_rset: "RAZ", + vu_sptp: "Haut", + vu_spfr: "Avant", + vu_splt: "Gauche", + vu_sprt: "Droite", + + // WORKSPACE + ws_menu: "vue", + ws_save: "Sauvegarder", + ws_cler: "Effacer", + + // OPTIONS + op_menu: "interface", + op_disp: "affichage", + op_xprt_s: "expert", + op_xprt_l: "afficher plus d'options de paramétrage", + op_decl_s: "logos", + op_decl_l: "montrer les logos des appareils", + op_dang_s: "expérimental", + op_dang_l: "montrer les paramètres expérimentaux", + op_hopo_s: "survol du menu", + op_hopo_l: ["activer le survol du menu", "pour activer"], + op_dark_s: "mode sombre", + op_dark_l: "interface en mode sombre", + op_comp_s: "ui compacte", + op_comp_l: ["interface utilisateur compacte", "plus adapté aux petits écrans", "et tablettes"], + op_shor_s: "montrer l'origine", + op_shor_l: "indiquer l'origine du dispositif ou du processus", + op_shru_s: "afficher les règles", + op_shru_l: ["montrer les règles des axes", "sur le quadrillage"], + op_sped_s: "afficher les vitesses", + op_sped_l: ["afficher la vitesse en barre de couleur","en mode prévisualisation"], + op_auto_s: "disposition automatique", + op_auto_l: ["disposition automatique sur le plateau", "lorsque de nouveaux éléments sont ajoutés"], + op_free_s: "disposition libre", + op_free_l: ["autoriser la disposition déplaçable","sans effet en mode laser"], + op_spcr_s: "espacement", + op_spcr_l: ["espacement entre les objets", "avec la disposition automatique", "en unités d'espace de travail"], + op_invr_s: "inverser zoom", + op_invr_l: ["inverser la molette de la souris", "faire défiler le zoom"], + op_save_s: "auto enregt", + op_save_l: ["préserve les objets dans l'espace de travail","entre rechargement de l'application"], + op_line_s: "type de ligne", + op_line_l: ["style de ligne pour le rendu","impacte les performances 3d","chemin: 3d meilleur","plat: 2d bon","ligne = 1d rapide"], + op_unit_s: "unités", + op_unit_l: ["les unités d'espace de travail affectent","vitesses et distances"], + op_anim_s: "animer", + op_anim_l: ["densité du maillage de l'animation","plus haut = plus dense","nécessite plus de mémoire","et est plus lent"], + + lo_menu: "disposition", + + pt_menu: "pièces", + pt_deci_s: "compression", + pt_deci_l: ["activer / désactiver la compression","durant l'importation. Tranchage plus rapide","et usage réduit de la mémoire"], + pt_qual_s: "qualité", + pt_qual_l: ["niveau de détail à garder", "lors des opérations de tranchage", "plus bas = plus rapide"], + pt_heal_s: "correction de maillage", + pt_heal_l: ["tentative de correction", "des mailles non-fermées", "accroit le temps de tranchage"], + + xp_menu: "exports", + + // SETTINGS + se_menu: "profil", + se_load: "charger", + se_save: "sauvegarder", + + // FDM SLICING + sl_menu: "couches", + sl_lahi_s: "hauteur", + sl_lahi_l: ["hauteur de chaque couche", "couche en millimètres"], + ad_minl_s: "hauteur min", + ad_minl_l: ["hauteur de couche minimale adaptative", "en millimètres", "doit être non nulle"], + sl_ltop_s: "couches du haut", + sl_ltop_l: ["nombre de couches solides", "à appliquer en", "haut de l'impression"], + sl_lsld_s: "couches solides", + sl_lsld_l: ["zones de remplissage solide calculées", "à partir des deltas de couches. voir", "menu surgissant des couches"], + sl_lbot_s: "couches de base", + sl_lbot_l: ["nombre de couches solides", "à appliquer en", "bas de l'impression"], + ad_adap_s: "adaptive", + ad_adap_l: ["utiliser des hauteurs de couche adaptatives", "avec 'hauteur de couche' en maximum", "et 'hauteur min' en minimum"], + + // FDM SHELLS + sl_shel_s: "parois", + sl_shel_l: ["nombre de périmètre", "murs à générer"], + sl_ordr_s: "ordre des parois", + sl_ordr_l: ["ordre d'impression des parois","de l'intérieur vers l'extérieur", "ou de l'extérieur vers l'intérieur","affecte la qualité de surface"], + sl_strt_s: "début de couche", + sl_strt_l: ["point de départ de la couche", "dernier = fin de la couche", "centre = centre de la pièce", "origine = origine du dispositif"], + ad_thin_s: "parois fines", + ad_thin_l: ["détecte et comble les vides", "entre les parois de la coque"], + + // FDM FILL + fi_menu: "remplissage", + fi_type: "type", + fi_pcnt_s: "fraction de remplissage", + fi_pcnt_l: ["valeurs de densité de remplissage","0.0 - 1.0"], + fi_angl_s: "départ solide", + fi_angl_l: ["angle de départ en degrés", "90 degrés ajoutés à ", "chaque couche suivante", "s'applique uniquement aux couches solides"], + fi_over_s: "chevauchement", + fi_over_l: ["chevauchement de l'enveloppe et du remplissage", "en tant que fraction du diamètre de la buse", "0,0 - 1,0"], + // fi_rate_s: "vitesse d'impression", + fi_rate_l: ["vitesse d'extrusion du remplissage", "régler à 0 pour utiliser par défaut", "la vitesse d'impression"], + + // FDM FIRST LAYER + fl_menu: "base", + fl_lahi_s: "hauteur de couche", + fl_lahi_l: ["hauteur de chaque couche", "en millimètres", "devrait être >= hauteur de la couche"], + fl_rate_s: "vitesse périmètres", + fl_rate_l: ["vitesse maximale d'impression des coques", "en millimètres / seconde"], + fl_frat_s: "vitesse remplissage", + fl_frat_l: ["vitesse maximale d'impression du remplissage", "en millimètres / seconde"], + fl_mult_s: "débit", + fl_mult_l: ["multiplicateur d'extrusion","0.0 - 2.0"], + fl_sfac_s: "largeur", + fl_sfac_l: ["multiplicateur de la taille de la buse", "modifie l'espacement des lignes"], + fl_skrt_s: "jupe nb", + fl_skrt_l: ["nombre de lignes de la jupe", "à générer pour la première couche"], + fl_skro_s: "décalage bordure", + fl_skro_l: ["jupe décalée de la partie", "en millimètres"], + fl_nozl_s: "buse", + fl_nozl_l: ["degrés en Celsius", "réglage de sortie utilisé", "lorsque celui-ci est égal à zéro"], + fl_bedd_s: "température plateau", + fl_bedd_l: ["degrés en Celsius", "réglage de sortie utilisé", "lorsque celui-ci est égal à zéro"], + fr_spac_s: "écart radeau", + fr_spac_l: ["espacement des couches supplémentaires", "entre la 1ère couche et le radeau", "en millimètres"], + fr_nabl_s: "activer un radeau", + fr_nabl_l: ["créer un radeau sous le ", "modèle pour une meilleure adhérence", "utilise le décalage des jupes et", "désactive la sortie des jupes"], + + // FDM BELT ONLY + fl_zoff_s: "décalage bande", + fl_zoff_l: ["décalage de hauteur par rapport à la bande", "de l'extrusion la plus basse", "en millimètres"], + fl_brim_s: "taille de bordure", + fl_brim_l: ["ajoute une bordure à la partie inférieure","la taille est la largeur en millimètres","0 désactive"], + fl_brmn_s: "seuil de bordure", + fl_brmn_l: ["ajout de bordures uniquement quand le segment", "face à la bande est plus court que cela", "valeur en millimètres", "0 = infini"], + fl_bled_s: "ancrage de pièce", + fl_bled_l: ["ancrage de la pièce sur la bande", "au début de l'impression", "en millimètres"], + + // FDM SUPPORT + sp_menu: "support", + sp_detect: "détecter", + sp_dens_s: "densité", + sp_dens_l: ["fraction 0.0 - 1.0","recommendé 0.15","0 désactive"], + sp_size_s: "taille pilier", + sp_size_l: ["largeur du pilier","en millimètres"], + sp_offs_s: "décalage du pilier", + sp_offs_l: ["décalage par rapport à la pièce","en millimètres"], + sp_gaps_s: "écart des couches", + sp_gaps_l: ["nombre de couches","de décalage de la pièce"], + sp_span_s: "portée maximale", + sp_span_l: ["portée non soutenue nécessitant", "un nouveau soutien à générer","en millimètres"], + sp_angl_s: "angle max", + sp_angl_l: ["angle de surplomb maximum avant", "de générer un pilier de soutien"], + sp_area_s: "surface min", + sp_area_l: ["surface minimale de","la colonne support","en millimètres"], + sp_xpnd_s: "étendre", + sp_xpnd_l: ["étendre la zone de soutien", "au-delà des limites d'une pièce","en millimètres"], + sp_nozl_s: "extrudeur", + sp_nozl_l: ["dans les systèmes multi-extrudeurs", "l'extrudeur à utiliser pour", "le matériau de support"], + sp_auto_s: "automatique", + sp_auto_l: ["activer les supports générés", "utiliser la géométrie at-slice-time", "les supports n'apparaîtront", "qu'une fois le tranchage terminé"], + + // LASER SLICING + ls_offs_s: "offset", + ls_offs_l: ["adjust for beam width","en millimètres"], + ls_lahi_s: "height", + ls_lahi_l: ["layer height","in millimeters","0 = auto/detect"], + ls_lahm_s: "min", + ls_lahm_l: ["layer height minimum","will merge auto slices","under this thickness","in millimeters"], + ls_sngl_s: "single", + ls_sngl_l: ["perform only one slice","at specified layer height"], + + // CNC COMMON terms + cc_tool: "tool", + cc_offs_s: "offset", + cc_offs_l: ["offset tool center","from chosen path"], + cc_spnd_s: "spindle rpm", + cc_spnd_l: ["spindle speed in","revolutions / minute"], + cc_sovr_s: "step over", + cc_sovr_l: ["as a fraction of","tool diameter"], + cc_sdwn_s: "step down", + cc_sdwn_l: ["step down depth","for each pass","in workspace units","0 to disable"], + cc_feed_s: "feed rate", + cc_feed_l: ["max cutting speed in","workspace units / minute"], + cc_plng_s: "plunge rate", + cc_plng_l: ["max z axis speed in","workspace units / minute"], + cc_sngl_s: "select lines only", + cc_sngl_l: ["select only single edges","instead of connected polylines"], + + // CNC COMMON + cc_menu: "limits", + cc_flip: "flip", + cc_rapd_s: "xy feed", + cc_rapd_l: ["max xy moves feedrate","in workspace units / minute"], + cc_rzpd_s: "z feed", + cc_rzpd_l: ["max z moves feedrate","in workspace units / minute"], + + cc_loff_s: "offset", + cc_loff_l: ["distance from stock face","for leveling pass","in workspace units"], + + // CNC ROUGHING + cr_menu: "rough", + cr_lsto_s: "leave stock", + cr_lsto_l: ["horizontal offset from vertical faces","stock to leave for finishing pass","in workspace units"], + cr_ease_s: "ease down", + cr_ease_l: ["plunge cuts will","spiral down or ease","along a linear path"], + cr_clrt_s: "clear top", + cr_clrt_l: ["run a clearing pass over","the bounding area of the part","at z = 0"], + cr_clrp_s: "clear voids", + cr_clrp_l: ["mill out through pockets","instead of just the outline"], + cr_clrf_s: "clear faces", + cr_clrf_l: ["interpolate step down to","clear any detected flat areas"], + cr_olin_s: "inside only", + cr_olin_l: ["limit cutting to","inside part boundaries"], + + // CNC OUTLINE + co_menu: "outline", + co_dogb_s: "dogbones", + co_dogb_l: ["insert dogbone cuts","into inside corners"], + co_wide_s: "wide cutout", + co_wide_l: ["widen outside cutout paths","for deep cuts in hard material"], + co_olin_s: "inside only", + co_olin_l: ["limit cutting to","inside part boundaries"], + co_olot_s: "outside only", + co_olot_l: ["limit cutting to","exterior part boundaries","which can be thought of","as the shadow outline"], + co_omit_s: "omit through", + co_omit_l: "eliminate thru holes", + co_olen_s: "enable", + co_olen_l: "enabled outline cutting", + + // CNC CONTOUR + cn_menu: "contour", + cf_angl_s: "max angle", + cf_angl_l: ["angles greater than this","are considered vertical"], + cf_curv_s: "curves only", + cf_curv_l: ["limit linear cleanup","to curved surfaces"], + cf_olin_s: "inside only", + cf_olin_l: ["limit cutting to","inside part boundaries"], + cf_linx_s: "enable y pass", + cf_linx_l: "linear y-axis finishing", + cf_liny_s: "enable x pass", + cf_liny_l: "linear x-axis finishing", + + // CNC TRACE + cu_menu: "trace", + cu_type_s: "type", + cu_type_l: ["follow = tool tip follows line","right or left = tool tip","follows line offset by tool radius"], + + // CNC DRILLING + cd_menu: "drill", + cd_axis: "axis", + cd_points: "points", + cd_plpr_s: "plunge per", + cd_plpr_l: ["max plunge between","dwell periods","in workspace units","0 to disable"], + cd_dwll_s: "dwell time", + cd_dwll_l: ["dwell time","between plunges in","in milliseconds"], + cd_lift_s: "drill lift", + cd_lift_l: ["lift between plunges","after dwell period","in workspace units","0 to disable"], + cd_regi_s: "register", + cd_regi_l: ["drill registration holes","for double-sided parts","independent of enable","drilling but uses same","tool and settings"], + + // CNC CUTOUT TABS + ct_menu: "tabs", + ct_angl_s: "angle", + ct_angl_l: ["starting angle for tab spacing","in degrees (0-360)"], + ct_numb_s: "count", + ct_numb_l: ["number of tabs to use","will be spaced evenly","around the part"], + ct_wdth_s: "width", + ct_wdth_l: "width in workspace units", + ct_hght_s: "height", + ct_hght_l: "height in workspace units", + ct_dpth_s: "depth", + ct_dpth_l: ["distance in workspace units","that the tab projects from","the part surface"], + ct_midl_s: "midline", + ct_midl_l: ["use midline of tab","instead of z bottom","for double-sided work"], + ct_nabl_s: "auto", + ct_nabl_l: ["auto generate radial tabs","projected from part center","using count and angle offset"], + + // OUTPUT + ou_menu: "sortie", + + // LASER KNIFE + dk_menu: "knife", + dk_dpth_s: "cut depth", + dk_dpth_l: ["final cut depth","in millimeters"], + dk_pass_s: "cut passes", + dk_pass_l: ["number of passes","down to cut depth"], + dk_offs_s: "tip offset", + dk_offs_l: ["distance from blade tip","to center of tool","in millimeters"], + + // OUTPUT LASER + ou_spac_s: "spacing", + ou_spac_l: ["distance between layer output","in millimeters"], + ou_scal_s: "scaling", + ou_scal_l: "multiplier (0.1 to 100)", + ou_powr_s: "power", + ou_powr_l: ["0 - 100","represents %"], + ou_sped_s: "speed", + ou_sped_l: "millimeters / second", + ou_mrgd_s: "merged", + ou_mrgd_l: ["merge all layers using","color coding to denote","stacking depth"], + ou_grpd_s: "grouped", + ou_grpd_l: ["retain each layer as","a unified grouping","instead of separated","polygons"], + ou_layr_s: "layer order", + ou_layr_l: ["output layer order","from top right to","bottom left"], + ou_layo_s: "layer color", + ou_layo_l: ["output layer colors","for each z index","overridden by merged"], + ou_drkn_s: "drag knife", + ou_drkn_l: ["enable drag knife","output in gcode","cut radii are added","to corners with","cut down passes"], + + // OUTPUT FDM + ou_nozl_s: "température buse", + ou_nozl_l: "degrés en celsius", + ou_bedd_s: "température plateau", + ou_bedd_l: "degrés en celsius", + ou_feed_s: "vitesse impression", + ou_feed_l: ["vitesse d'impression maximale","millimètres / seconde"], + ou_fini_s: "vitesse coque", + ou_fini_l: ["vitesse de la coque extérieure","millimètres / seconde"], + ou_move_s: "vitesse déplacement", + ou_move_l: ["vitesse de déplacement sans impression","millimètres / seconde","0 = active déplacements G0"], + ou_shml_s: "débit de coque", + ou_flml_s: "débit de solidité", + ou_spml_s: "débit de remplissage", + ou_exml_l: ["multiplicateur d'extrusion","0.0 - 2.0"], + ou_fans_s: "vitesse ventilateur", + ou_fans_l: "0 - 255", + + // OUTPUT CAM + ou_toll_s: "tolerance", + ou_toll_l: ["surface precision","in workspace units","lower is slower and","uses more memory","0 = automatic based","on animate preference"], + ou_zanc_s: "z anchor", + ou_zanc_l: ["controls the position of the part","when stock Z exceeds part Z"], + ou_ztof_s: "z offset", + ou_ztof_l: ["offset z anchor","in workspace units","has no effect when","anchor is middle"], + ou_zbot_s: "z bottom", + ou_zbot_l: ["offset from part bottom","to limit cutting depth","in workspace units"], + ou_zclr_s: "z clearance", + ou_zclr_l: ["safe travel offset","from top of part","in workspace units"], + ou_ztru_s: "z thru", + ou_ztru_l: ["extend cutout pass down","in workspace units"], + ou_conv_s: "conventional", + ou_conv_l: ["milling direction","uncheck for 'climb'"], + ou_depf_s: "depth first", + ou_depf_l: ["optimize pocket cuts","with depth priority"], + + // CAM STOCK + cs_menu: "stock", + cs_wdth_s: "width", + cs_wdth_l: ["width (x) in workspace units","0 defaults to part size"], + cs_dpth_s: "depth", + cs_dpth_l: ["depth (y) in workspace units","0 defaults to part size"], + cs_hght_s: "height", + cs_hght_l: ["height (z) in workspace units","0 defaults to part size"], + cs_offs_s: "offset", + cs_offs_l: ["use width, depth, height","as offsets from max","part size on platform"], + cs_clip_s: "clip to", + cs_clip_l: ["rough and outline","clip cutting paths","to defined stock"], + cs_offe_s: "enable", + cs_offe_l: "enable milling stock", + + // ORIGIN (CAM & LASER) + or_bnds_s: "origin bounds", + or_bnds_l: ["origin is relative to","boundary of all objects"], + or_cntr_s: "origin center", + or_cntr_l: "origin is referenced from the center", + or_topp_s: "origin top", + or_topp_l: "origin is references from the top of objects", + + // FDM ADVANCED + ad_menu: "expert", + ad_rdst_s: "distance de rétraction", + ad_rdst_l: ["longueur de filament à rétracter", "pour les longs mouvements. en millimètres"], + ad_rrat_s: "vitesse de rétraction", + ad_rrat_l: ["vitesse de rétraction","du filament en mm/s"], + ad_rdwl_s: "réenclencher", + ad_wpln_s: "essuyage rétractation", + ad_wpln_l: ["mouvement sans impression", "après rétraction", "en millimètres"], + ad_rdwl_l: ["durée de réenclenchement", "filament et mouvement", "en millisecondes"], + ad_scst_s: "cabotage", + ad_scst_l: ["extrémité non imprimée", "des coques du périmètre", "en millimètres"], + ad_msol_s: "min solide", + ad_msol_l: ["aire minimale (mm^2)","requise pour rester solide","doit être > 0.1"], + ad_mins_s: "vitesse minimale", + ad_mins_l: ["vitesse minimale","des segments courts"], + ad_spol_s: "chemin court", + ad_spol_l: ["les polygones plus courts", "auront la vitesse réduite et", "seront ramenés à la vitesse minimale", "en millimètres"], + ad_arct_s: "tolérance arc", + ad_arct_l: ["convertir les lignes des facettes en arcs", "tolérance de dérive du point central", "lors de la correspondance des points d'arc", "considérer les valeurs autour de 0,15", "en millimètres", "0 désactive"], + ad_zhop_s: "décalage en Z", + ad_zhop_l: ["levée du z","sur les mouvements de rétraction", "en millimètres", "0 désactive"], + ad_abkl_s: "anti-backlash", + ad_abkl_l: ["pour une meilleure finition de la surface plane", "utiliser des micro-mouvements pour annuler", "le jeu dans la sortie de la couche solide", "en millimètres", "0 désactive", "si le firmware reconnait le M425", "l'utiliser dans l'en-tête du gcode", "et mettre à 0"], + ad_lret_s: "rétraction au changement de couche", + ad_lret_l: ["forcer la rétraction du filament", "entre les couches"], + ad_play_s: "couches de polissage", + ad_play_l: ["polir jusqu'au nombre spécifié", "# de couches à la fois"], + ad_pspd_s: "vitesse de polissage", + ad_pspd_l: ["vitesse de polissage", "en millimètres / seconde"], + + // CAM EXPERT + cx_fast_s: "skip shadow", + cx_fast_l: ["disable overhang detection","can be faster and use less","memory with complex models","but fails with overhangs","try enabling if slicing","hangs during shadowing"], + + // FDM GCODE + ag_menu: "gcode", + ag_nozl_s: "buse", + ag_nozl_l: "sélectionner la buse ou la tête d'impression", + ag_paws_s: "couches de pause", + ag_paws_l: ["liste de couches séparées par des virgules", "pour injecter des commandes de pause avant"], + ag_loop_s: "couches de répétitions", + ag_loop_l: ["des plages de couches à répéter dans le format ", "premier-compte dernier,premier-compte dernier,...", "compte omis = 1"], + + // SLA MENU + sa_menu: "tranchage", + sa_lahe_s: "hauteur de couche", + sa_lahe_l: ["hauteur de couche","en millimètres"], + sa_shel_s: "hollow shell", + sa_shel_l: ["shell thickness in mm","use multiple of layer height","use 0 for solid (disabled)"], + sa_otop_s: "open top", + sa_otop_l: ["if shell is enabled","results in an open top"], + sa_obas_s: "open base", + sa_obas_l: ["if shell is enabled","results in an open base","disabled with supports"], + + sa_layr_m: "layers", + sa_lton_s: "light on time", + sa_lton_l: ["layer light on","time in seconds"], + sa_ltof_s: "light off time", + sa_ltof_l: ["layer light off","time in seconds"], + sa_pldi_s: "peel distance", + sa_pldi_l: ["peel distance","in millimeters"], + sa_pllr_s: "peel lift rate", + sa_pllr_l: ["peel lift speed","in mm/sec"], + sa_pldr_s: "peel drop rate", + sa_pldr_l: ["peel drop speed","in mm/sec"], + + sa_base_m: "base", + sa_balc_s: "layer count", + sa_balc_l: ["number of","base layers"], + sa_bltn_l: ["base layer light on","time in seconds"], + sa_bltf_l: ["base layer light off","time in seconds"], + + sa_infl_m: "infill", + sa_ifdn_s: "density", + sa_ifdn_l: ["percent infill","requires shell","0 = disabled","valid 0.0 - 1.0"], + sa_iflw_s: "line width", + sa_iflw_l: ["hatch line width","in millimeters"], + + sa_supp_m: "support", + sa_slyr_s: "base layers", + sa_slyr_l: ["base support layers","value range 0-10"], + sa_slgp_s: "gap layers", + sa_slgp_l: ["number of layers between","raft and bottom of object"], + sa_sldn_s: "density", + sa_sldn_l: ["used to compute the","number of support pillars","0.0-1.0 (0 = disable)"], + sa_slsz_s: "size", + sa_slsz_l: ["max size of a","support pillar","in millimeters"], + sa_slpt_s: "points", + sa_slpt_l: ["number of points in","each support pillar","in millimeters"], + sl_slen_l: "enable supports", + + sa_outp_m: "output", + sa_opzo_s: "z offset", + sa_opzo_l: ["z layer offset","almost always 0.0","0.0-1.0 in millimeters"], + sa_opaa_s: "anti alias", + sa_opaa_l: ["enable anti-aliasing","produces larger files","can blur details"] +}; diff --git a/web2/kiri/lang/pl-pl.js b/web2/kiri/lang/pl-pl.js new file mode 100644 index 00000000..2545e6e7 --- /dev/null +++ b/web2/kiri/lang/pl-pl.js @@ -0,0 +1,645 @@ +// Polish. Język polski tłumaczony na zasadach własności publicznej +kiri.lang['pl'] = +kiri.lang['pl-pl'] = { + // hasło i tłumaczenie + animate: "animacja", // CAM animate button + arrange: "rozmieść", // layout workspace objects + axis: "osie", // left object scale pop menu + clear: "wyczyść", //"czyszczenie z obiektów", // clear workspace (remove all objects) + copy: "kopiowanie", + delete: "kasowanie", + detail: "szczegóły", + done: "wykonaj", + enable: "włącz", + export: "eksport", + files: "pliki", + help: "pomoc", + ghost: "druciak", // left render pop menu (wireframe) + hide: "ukryj", // left render pop menu (invisible) + home: "początek", + import: "import", + language: "język", + machine: "maszyna", // device or machine + metric: "metryka", + name: "nazwa", + prefs: "ustawienia", // left menu "preferences" + preview: "podgląd", + recent: "recent", + render: "render", // left render pop menu + reset: "reset", + rotate: "obróć", // left rotate pop menu + save: "zapisz", + scale: "skala", // left object scale pop menu + setup: "ustaw", + settings: "ustawienia", + size: "wielkość", + slice: "plaserki", + solid: "wypełniony", // view type pop menu + start: "start", + tool: "narzędzie", + tools: "narzędzia", // CAM tool menu button + top: "góra", + type: "typ", // CAM tool type + version: "wersja", + view: "widok", // left view pop menu + wire: "druciak", // left render pop menu "wire" + + acct_xpo: ["zrób backup swojego urządzenia","i profil urządzenia z","opcja by zapisać przestrzeń roboczą","obiekty i pozycje"], + + // RIGHT-CLICK CONTEXT MENU + rc_clws: "wyczyść przestrzeń roboczą", + rc_xpws: "wyeksportuj przestrzeń roboczą", + rc_lafl: "ułóż płasko", + rc_mirr: "odbicie", // lustro + rc_dupl: "skopiuj", + rc_xstl: "zapisz w formacie STL", + + // DEVICE MENU and related dialogs + dm_sldt: "wybierz rodzaj swojego urządzenia", + dm_stdd: "zwykłe urządzenia", + dm_mydd: "moje urządzenia", + dm_seld: "wybierz urządzenie", + dm_rcnt: "ostatnio uzywane pliki", + dm_savs: "zachowane ustawienia", + dm_appp: "Ustawienia programu", + + // CAM Tool Dialog + td_tyem: "tępo zakończone", // end mill + td_tybm: "kula na końcu", // ball mill + td_tytm: "zwężający się", // taper mill + td_tonm: "narzędzie #", + td_shft: "wał (pionowy)", // endmill shaft specs + td_flut: "czołowy", // endmill flute specs + td_tapr: "zwężający się", // endmill taper specs + + // DEVICE dialog groups + dv_gr_dev: "urządzenie", + dv_gr_ext: "ekstruder", + dv_gr_out: "wynik", + dv_gr_gco: "makra gkodu", + + // DEVICE dialog (_s = label, _l = hover help) + dv_name_s: "nazwa", + dv_name_l: "nazwa urządzenia", + dv_fila_s: "filament", + dv_fila_l: "średnica w milimetrach", + dv_nozl_s: "dysza", + dv_nozl_l: "średnica w milimetrach", + dv_bedw_s: "szerokość", + dv_bedw_l: "pole robocze w jednostkach", + dv_bedd_s: "głębokość", + dv_bedd_l: "pole robocze w jednostkach", + dv_bedh_s: "wysokość", + dv_bedh_l: ["maksymalna wysokość budowania","w jednostkach pola roboczego"], + dv_spmx_s: "maksymalna prędkość wrzeciona", + dv_spmx_l: ["maksymalna prędkość wrzeciona w obrotach na minutę","0 wyłączone"], + dv_xtab_s: "ustawienie pozycji absolutnie", + dv_xtab_l: "absolutna wartość wypchnięcia", + dv_orgc_s: "środek odniesienia", + dv_orgc_l: "środek odniesienia na platformie", + // dv_orgt_s: "odniesienie u góry", + // dv_orgt_l: "oś z odniesienie u góry", + dv_bedc_s: "okrądła platforma drukowania", + dv_bedc_l: "platforma druku jest okrągła", + dv_belt_s: "pas na platformie", + dv_belt_l: "kontynuacja drukowania platformy", + dv_retr_s: "domyślne cofnięcie (retrakcja)", + dv_retr_l: ["firmware urządzenia wspiera kody G10/G11"], + dv_fanp_s: "zasilanie wiatraczków", + dv_fanp_l: "ustawienia zasilania wiatraczków", + dv_prog_s: "postęp", + dv_prog_l: "wynik w czasie każdego % postępu", + dv_layr_s: "warstwa", + dv_layr_l: ["output at each","layer change"], + dv_tksp_s: "token spacer", + dv_tksp_l: ["add a space between","gcode axis parameters","G0X0Y0X0","vs","G0 X0 Y0 Z0"], + dv_strc_s: "strip comments", + dv_strc_l: ["strip gcode comments","comments begin with ;"], + dv_fext_s: "rozszerzenie pliku", + dv_fext_l: "rozszerzenie pliku", + dv_dwll_s: "dwell", + dv_dwll_l: "gcode dwell script", + dv_tool_s: "zmiana narzędzia", + dv_tool_l: "skrypt zmiany narzędzia", + dv_sspd_s: "prędkość wrzeciona", + dv_sspd_l: "ustaw prędkość wrzeciona", + dv_paus_s: "pauza", + dv_paus_l: "gkod skryptu pauzy", + dv_head_s: "nagłówek (początek)", + dv_head_l: "skrypt gkod nagłówka", + dv_foot_s: "stopka (zakończenie)", + dv_foot_l: "skrypt gkod stopki", + dv_lzon_s: "laser włączony", + dv_lzon_l: "gcode laser on script", + dv_lzof_s: "laser wyłączony", + dv_lzof_l: "gcode laser off script", + dv_exts_s: "zaznacz", + dv_exts_l: "gcode run to enable this extruder", + dv_dext_s: "odznacz", + dv_dext_l: "gcode run before enabling another extruder", + dv_extd_s: "deselect", + dv_extd_l: "gcode to deselect this extruder", + dv_exox_s: "offset x", + dv_exox_l: "nozzle offset x", + dv_exoy_s: "offset y", + dv_exoy_l: "nozzle offset y", + + // MODE + mo_menu: "tryb", + mo_fdmp: "FDM Druk przyrostowy", + mo_slap: "SLA Druk przyrostowy", + mo_lazr: "Ciecie laserem", + mo_cncm: "Frezowanie CNC", + + // SETUP + su_menu: "ustawienia", + su_devi: "Urządzenia", + su_tool: "Narzędzia", + su_locl: "Lokalnie", + su_xprt: "Eksport", + su_help: "Pomoc", + + // LOAD + fe_menu: "plik", + fn_recn: "ostatnie pliki", + fn_impo: "Importowanie", + + // FUNCTION + fn_menu: "action", + fn_arra: "Arrange", + fn_slic: "Slice", + fn_prev: "Preview", + fn_expo: "Export", + + // VIEW + vu_menu: "view", + vu_home: "Home", + vu_rset: "Reset", + vu_sptp: "Top", + vu_spfr: "Front", + vu_splt: "Left", + vu_sprt: "Right", + + // WORKSPACE + ws_menu: "view", + ws_save: "Save", + ws_cler: "Clear", + + // OPTIONS + op_menu: "interface", + op_disp: "display", + op_xprt_s: "expert", + op_xprt_l: "show more settings options", + op_decl_s: "decals", + op_decl_l: "show device decals and logos", + op_dang_s: "experimental", + op_dang_l: "show experimental parameters", + op_hopo_s: "hover pop", + op_hopo_l: ["enable menu hover","to activate"], + op_dark_s: "dark mode", + op_dark_l: "dark mode interface", + op_comp_s: "compact ui", + op_comp_l: ["compact user interface","better for small screens","and tablets"], + op_shor_s: "show origin", + op_shor_l: "show device or process origin", + op_shru_s: "show rulers", + op_shru_l: ["show axes rulers","on major gridlines"], + op_sped_s: "show speeds", + op_sped_l: ["show speed to color bar","in preview mode"], + op_auto_s: "auto layout", + op_auto_l: ["automatically layout platform","when new items added"], + op_free_s: "free layout", + op_free_l: ["permit dragable layout","no effect in laser mode"], + op_spcr_s: "spacing", + op_spcr_l: ["spacing between objects","during auto layout","in workspace units"], + op_orth_s: "orthographic", + op_orth_l: ["orthographic display","requires page refresh"], + op_invr_s: "invert zoom", + op_invr_l: ["invert mouse wheel","scroll zoom"], + op_save_s: "auto save", + op_save_l: ["preserve objects in workspace","between application reloads"], + op_line_s: "line type", + op_line_l: ["line style for path rendering","impacts 3d performance","path: 3d best","flat: 2d good","line = 1d fast"], + op_unit_s: "units", + op_unit_l: ["workspace units affects","speeds and distances"], + op_anim_s: "animate", + op_anim_l: ["animation mesh density","higher is more dense","takes more memory","and is slower"], + + lo_menu: "layout", + + pt_menu: "parts", + pt_deci_s: "decimate", + pt_deci_l: ["enable or disable point decimation","during port import. for faster slicing","and lower memory usage"], + pt_qual_s: "quality", + pt_qual_l: ["level of detail to retain","during slicing operations","lower is faster"], + pt_heal_s: "heal mesh", + pt_heal_l: ["attempt to heal","non-manifold meshes","adds to slicing time"], + + xp_menu: "exports", + + // SETTINGS + se_menu: "profile", + se_load: "load", + se_save: "save", + + // FDM SLICING + sl_menu: "layers", + sl_lahi_s: "height", + sl_lahi_l: ["height of each slice","layer in millimeters"], + ad_minl_s: "height min", + ad_minl_l: ["adaptive min layer height","in millimeters","must be non-zero"], + sl_ltop_s: "top layers", + sl_ltop_l: ["number of solid layers","to enforce at the","top of the print"], + sl_lsld_s: "solid layers", + sl_lsld_l: ["solid fill areas computed","from layer deltas. see","layer pop menu"], + sl_lbot_s: "base layers", + sl_lbot_l: ["number of solid layers","to enforce at the","bottom of the print"], + ad_adap_s: "adaptive", + ad_adap_l: ["use adaptive layer heights","with 'layer height' as max","and 'layer min' as the min"], + + // FDM SHELLS + sl_shel_s: "shell count", + sl_shel_l: ["number of perimeter","walls to generate"], + sl_ordr_s: "shell order", + sl_ordr_l: ["output shell order","inside to outside","or outside to inside","affects surface quality"], + sl_strt_s: "layer start", + sl_strt_l: ["layer starting point","last = last layer end","center = part center","origin = device origin"], + ad_thin_s: "thin walls", + ad_thin_l: ["detect and fill gaps","between shell walls"], + + // FDM FILL + fi_menu: "infill", + fi_type: "type", + fi_pcnt_s: "fill fraction", + fi_pcnt_l: ["fill density values","0.0 - 1.0"], + fi_angl_s: "solid start", + fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], + fi_over_s: "overlap", + fi_over_l: ["overlap with shell and other fill","as fraction of nozzle diameter","0.0 - 1.0"], + // fi_rate_s: "print speed", + fi_rate_l: ["extrusion speed for infill","set to 0 to use default","output print speeds"], + + // FDM FIRST LAYER + fl_menu: "base", + fl_lahi_s: "layer height", + fl_lahi_l: ["height of each slice","in millimeters","should be >= slice height"], + fl_rate_s: "shell speed", + fl_rate_l: ["shell printing max speed","in millimeters / second"], + fl_frat_s: "fill speed", + fl_frat_l: ["fill printing max speed","in millimeters / second"], + fl_mult_s: "flow factor", + fl_mult_l: ["extrusion multiplier","0.0 - 2.0"], + fl_sfac_s: "width factor", + fl_sfac_l: ["multiplier on nozzle size","changes line spacing"], + fl_skrt_s: "skirt count", + fl_skrt_l: ["number of first-layer offset","brims to generate"], + fl_skro_s: "skirt offset", + fl_skro_l: ["skirt offset from part","in millimeters"], + fl_nozl_s: "nozzle temp", + fl_nozl_l: ["degrees in celsius","output setting used","when this is zero"], + fl_bedd_s: "bed temp", + fl_bedd_l: ["degrees in celsius","output setting used","when this is zero"], + fr_spac_s: "raft gap", + fr_spac_l: ["additional layer spacing","between 1st layer and raft","in millimeters"], + fr_nabl_s: "raft enable", + fr_nabl_l: ["create a raft under the","model for better adhesion","uses skirt offset and","disables skirt output"], + + // FDM BELT ONLY + fl_zoff_s: "belt offset", + fl_zoff_l: ["height offset from belt","of lowest extrusion","in millimeters"], + fl_brim_s: "brim size", + fl_brim_l: ["add brim to part bottom","size is width in millimeters","0 to disable"], + fl_brmn_s: "brim trigger", + fl_brmn_l: ["add brims only when segment","facing belt is shorter than this","value in millimeters","0 = Infinity"], + fl_bled_s: "part anchor", + fl_bled_l: ["belt part anchor","at start of print","in millimeters"], + + // FDM SUPPORT + sp_menu: "support", + sp_detect: "detect", + sp_dens_s: "density", + sp_dens_l: ["fraction 0.0 - 1.0","recommended 0.15","0 to disable"], + sp_size_s: "pillar size", + sp_size_l: ["pillar width","in millimeters"], + sp_offs_s: "part offset", + sp_offs_l: ["offset from part","in millimeters"], + sp_gaps_s: "gap layers", + sp_gaps_l: ["number of layers","offset from part"], + sp_span_s: "max span", + sp_span_l: ["unsupported span that causes","a new support to be generated","in millimeters"], + sp_angl_s: "max angle", + sp_angl_l: ["max overhang angle before","supporting pillar is generated"], + sp_area_s: "min area", + sp_area_l: ["minimum area for","a support column","in millimeters"], + sp_xpnd_s: "expand", + sp_xpnd_l: ["expand support area","beyond part boundary","in millimeters"], + sp_nozl_s: "extruder", + sp_nozl_l: ["in multi-extruder systems","the extruder to use for","support material"], + sp_auto_s: "automatic", + sp_auto_l: ["enable generated supports","using at-slice-time geometry","supports will only appear","after slicing completes"], + + // LASER SLICING + ls_offs_s: "offset", + ls_offs_l: ["adjust for beam width","in millimeters"], + ls_lahi_s: "height", + ls_lahi_l: ["layer height","in millimeters","0 = auto/detect"], + ls_lahm_s: "min", + ls_lahm_l: ["layer height minimum","will merge auto slices","under this thickness","in millimeters"], + ls_sngl_s: "single", + ls_sngl_l: ["perform only one slice","at specified layer height"], + + // CNC COMMON terms + cc_tool: "tool", + cc_offs_s: "offset", + cc_offs_l: ["offset tool center","from chosen path"], + cc_spnd_s: "spindle rpm", + cc_spnd_l: ["spindle speed in","revolutions / minute"], + cc_sovr_s: "step over", + cc_sovr_l: ["as a fraction of","tool diameter"], + cc_sdwn_s: "step down", + cc_sdwn_l: ["step down depth","for each pass","in workspace units","0 to disable"], + cc_feed_s: "feed rate", + cc_feed_l: ["max cutting speed in","workspace units / minute"], + cc_plng_s: "plunge rate", + cc_plng_l: ["max z axis speed in","workspace units / minute"], + cc_sngl_s: "select lines only", + cc_sngl_l: ["select only single edges","instead of connected polylines"], + + // CNC COMMON + cc_menu: "limits", + cc_flip: "flip", + cc_rapd_s: "xy feed", + cc_rapd_l: ["max xy moves feedrate","in workspace units / minute"], + cc_rzpd_s: "z feed", + cc_rzpd_l: ["max z moves feedrate","in workspace units / minute"], + + cc_loff_s: "offset", + cc_loff_l: ["distance from stock face","for leveling pass","in workspace units"], + + // CNC ROUGHING + cr_menu: "rough", + cr_lsto_s: "leave stock", + cr_lsto_l: ["horizontal offset from vertical faces","stock to leave for finishing pass","in workspace units"], + cr_ease_s: "ease down", + cr_ease_l: ["plunge cuts will","spiral down or ease","along a linear path"], + cr_clrt_s: "clear top", + cr_clrt_l: ["run a clearing pass over","the bounding area of the part","at z = 0"], + cr_clrp_s: "clear voids", + cr_clrp_l: ["mill out through pockets","instead of just the outline"], + cr_clrf_s: "clear faces", + cr_clrf_l: ["interpolate step down to","clear any detected flat areas"], + cr_olin_s: "inside only", + cr_olin_l: ["limit cutting to","inside part boundaries"], + + // CNC OUTLINE + co_menu: "outline", + co_dogb_s: "dogbones", + co_dogb_l: ["insert dogbone cuts","into inside corners"], + co_wide_s: "wide cutout", + co_wide_l: ["widen outside cutout paths","for deep cuts in hard material"], + co_olin_s: "inside only", + co_olin_l: ["limit cutting to","inside part boundaries"], + co_olot_s: "outside only", + co_olot_l: ["limit cutting to","exterior part boundaries","which can be thought of","as the shadow outline"], + co_omit_s: "omit through", + co_omit_l: "eliminate thru holes", + co_olen_s: "enable", + co_olen_l: "enabled outline cutting", + + // CNC CONTOUR + cn_menu: "contour", + cf_angl_s: "max angle", + cf_angl_l: ["angles greater than this","are considered vertical"], + cf_curv_s: "curves only", + cf_curv_l: ["limit linear cleanup","to curved surfaces"], + cf_olin_s: "inside only", + cf_olin_l: ["limit cutting to","inside part boundaries"], + cf_linx_s: "enable y pass", + cf_linx_l: "linear y-axis finishing", + cf_liny_s: "enable x pass", + cf_liny_l: "linear x-axis finishing", + + // CNC TRACE + cu_menu: "trace", + cu_type_s: "type", + cu_type_l: ["follow = tool tip follows line","right or left = tool tip","follows line offset by tool radius"], + + // CNC DRILLING + cd_menu: "drill", + cd_axis: "axis", + cd_points: "points", + cd_plpr_s: "plunge per", + cd_plpr_l: ["max plunge between","dwell periods","in workspace units","0 to disable"], + cd_dwll_s: "dwell time", + cd_dwll_l: ["dwell time","between plunges in","in milliseconds"], + cd_lift_s: "drill lift", + cd_lift_l: ["lift between plunges","after dwell period","in workspace units","0 to disable"], + cd_regi_s: "register", + cd_regi_l: ["drill registration holes","for double-sided parts","independent of enable","drilling but uses same","tool and settings"], + + // CNC CUTOUT TABS + ct_menu: "tabs", + ct_angl_s: "angle", + ct_angl_l: ["starting angle for tab spacing","in degrees (0-360)"], + ct_numb_s: "count", + ct_numb_l: ["number of tabs to use","will be spaced evenly","around the part"], + ct_wdth_s: "width", + ct_wdth_l: "width in workspace units", + ct_hght_s: "height", + ct_hght_l: "height in workspace units", + ct_dpth_s: "depth", + ct_dpth_l: ["distance in workspace units","that the tab projects from","the part surface"], + ct_midl_s: "midline", + ct_midl_l: ["use midline of tab","instead of z bottom","for double-sided work"], + ct_nabl_s: "auto", + ct_nabl_l: ["auto generate radial tabs","projected from part center","using count and angle offset"], + + // OUTPUT + ou_menu: "output", + + // LASER KNIFE + dk_menu: "knife", + dk_dpth_s: "cut depth", + dk_dpth_l: ["final cut depth","in millimeters"], + dk_pass_s: "cut passes", + dk_pass_l: ["number of passes","down to cut depth"], + dk_offs_s: "tip offset", + dk_offs_l: ["distance from blade tip","to center of tool","in millimeters"], + + // OUTPUT LASER + ou_spac_s: "spacing", + ou_spac_l: ["distance between layer output","in millimeters"], + ou_scal_s: "scaling", + ou_scal_l: "multiplier (0.1 to 100)", + ou_powr_s: "power", + ou_powr_l: ["0 - 100","represents %"], + ou_sped_s: "speed", + ou_sped_l: "millimeters / second", + ou_mrgd_s: "merged", + ou_mrgd_l: ["merge all layers using","color coding to denote","stacking depth"], + ou_grpd_s: "grouped", + ou_grpd_l: ["retain each layer as","a unified grouping","instead of separated","polygons"], + ou_layr_s: "layer order", + ou_layr_l: ["output layer order","from top right to","bottom left"], + ou_layo_s: "layer color", + ou_layo_l: ["output layer colors","for each z index","overridden by merged"], + ou_drkn_s: "drag knife", + ou_drkn_l: ["enable drag knife","output in gcode","cut radii are added","to corners with","cut down passes"], + + // OUTPUT FDM + ou_nozl_s: "nozzle temp", + ou_nozl_l: "degrees in celsius", + ou_bedd_s: "bed temp", + ou_bedd_l: "degrees in celsius", + ou_feed_s: "print speed", + ou_feed_l: ["max print speed","millimeters / second"], + ou_fini_s: "finish speed", + ou_fini_l: ["outermost shell speed","millimeters / second"], + ou_move_s: "move speed", + ou_move_l: ["non-print move speed","millimeters / second","0 = enable G0 moves"], + ou_shml_s: "shell factor", + ou_flml_s: "solid factor", + ou_spml_s: "infill factor", + ou_exml_l: ["extrusion multiplier","0.0 - 2.0"], + ou_fans_s: "fan speed", + ou_fans_l: "0 - 255", + + // OUTPUT CAM + ou_toll_s: "tolerance", + ou_toll_l: ["surface precision","in workspace units","lower is slower and","uses more memory","0 = automatic based","on animate preference"], + ou_zanc_s: "z anchor", + ou_zanc_l: ["controls the position of the part","when stock Z exceeds part Z"], + ou_ztof_s: "z offset", + ou_ztof_l: ["offset z anchor","in workspace units","has no effect when","anchor is middle"], + ou_zbot_s: "z bottom", + ou_zbot_l: ["offset from part bottom","to limit cutting depth","in workspace units"], + ou_zclr_s: "z clearance", + ou_zclr_l: ["safe travel offset","from top of part","in workspace units"], + ou_ztru_s: "z thru", + ou_ztru_l: ["extend cutout pass down","in workspace units"], + ou_conv_s: "conventional", + ou_conv_l: ["milling direction","uncheck for 'climb'"], + ou_depf_s: "depth first", + ou_depf_l: ["optimize pocket cuts","with depth priority"], + + // CAM STOCK + cs_menu: "stock", + cs_wdth_s: "width", + cs_wdth_l: ["width (x) in workspace units","0 defaults to part size"], + cs_dpth_s: "depth", + cs_dpth_l: ["depth (y) in workspace units","0 defaults to part size"], + cs_hght_s: "height", + cs_hght_l: ["height (z) in workspace units","0 defaults to part size"], + cs_offs_s: "offset", + cs_offs_l: ["use width, depth, height","as offsets from max","part size on platform"], + cs_clip_s: "clip to", + cs_clip_l: ["rough and outline","clip cutting paths","to defined stock"], + cs_offe_s: "enable", + cs_offe_l: "enable milling stock", + + // ORIGIN (CAM & LASER) + or_bnds_s: "origin bounds", + or_bnds_l: ["origin is relative to","boundary of all objects"], + or_cntr_s: "origin center", + or_cntr_l: "origin is referenced from the center", + or_topp_s: "origin top", + or_topp_l: "origin is references from the top of objects", + + // FDM ADVANCED + ad_menu: "expert", + ad_rdst_s: "retract dist", + ad_rdst_l: ["amount to retract filament","for long moves. in millimeters"], + ad_rrat_s: "retract rate", + ad_rrat_l: ["speed of filament","retraction in mm/s"], + ad_rdwl_s: "engage dwell", + ad_wpln_s: "retract wipe", + ad_wpln_l: ["non-printing move","after retraction","in millimeters"], + ad_rdwl_l: ["time between re-engaging","filament and movement","in milliseconds"], + ad_scst_s: "shell coast", + ad_scst_l: ["non-printing end","of perimeter shells","in millimeters"], + ad_msol_s: "min solid", + ad_msol_l: ["minimum area (mm^2)","required to keep solid","must be > 0.1"], + ad_mins_s: "min speed", + ad_mins_l: ["minimum speed","for short segments"], + ad_spol_s: "short path", + ad_spol_l: ["polygons shorter than this","will have their print speed","scaled down to min speed","in millimeters"], + ad_arct_s: "arc tolerance", + ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","in millimeters","0 to disable"], + ad_zhop_s: "z hop dist", + ad_zhop_l: ["amount to raise z","on retraction moves","in millimeters","0 to disable"], + ad_abkl_s: "anti-backlash", + ad_abkl_l: ["for better flat surface finish","use micro-movements to cancel","backlash in solid layer output","in millimeters","0 to disable","if your firmware has M425","put that in the gcode header","and leave this as 0"], + ad_lret_s: "layer retract", + ad_lret_l: ["force filament retraction","between layers"], + ad_play_s: "polish layers", + ad_play_l: ["polish up to specified","# of layers at a time"], + ad_pspd_s: "polish speed", + ad_pspd_l: ["polishing speed","in millimeters / second"], + + // CAM EXPERT + cx_fast_s: "skip shadow", + cx_fast_l: ["disable overhang detection","can be faster and use less","memory with complex models","but fails with overhangs","try enabling if slicing","hangs during shadowing"], + + // FDM GCODE + ag_menu: "gcode", + ag_nozl_s: "nozzle", + ag_nozl_l: "select output nozzle or head", + ag_paws_s: "pause layers", + ag_paws_l: ["comma-separated list of layers","to inject pause commands before"], + ag_loop_s: "loop layers", + ag_loop_l: ["layer ranges to repeat in the format","first-last-count,first-last-count,...","omitted count = 1"], + + // SLA MENU + sa_menu: "slicing", + sa_lahe_s: "layer height", + sa_lahe_l: ["layer height","in millimeters"], + sa_shel_s: "hollow shell", + sa_shel_l: ["shell thickness in mm","use multiple of layer height","use 0 for solid (disabled)"], + sa_otop_s: "open top", + sa_otop_l: ["if shell is enabled","results in an open top"], + sa_obas_s: "open base", + sa_obas_l: ["if shell is enabled","results in an open base","disabled with supports"], + + sa_layr_m: "layers", + sa_lton_s: "light on time", + sa_lton_l: ["layer light on","time in seconds"], + sa_ltof_s: "light off time", + sa_ltof_l: ["layer light off","time in seconds"], + sa_pldi_s: "peel distance", + sa_pldi_l: ["peel distance","in millimeters"], + sa_pllr_s: "peel lift rate", + sa_pllr_l: ["peel lift speed","in mm/sec"], + sa_pldr_s: "peel drop rate", + sa_pldr_l: ["peel drop speed","in mm/sec"], + + sa_base_m: "base", + sa_balc_s: "layer count", + sa_balc_l: ["number of","base layers"], + sa_bltn_l: ["base layer light on","time in seconds"], + sa_bltf_l: ["base layer light off","time in seconds"], + + sa_infl_m: "infill", + sa_ifdn_s: "density", + sa_ifdn_l: ["percent infill","requires shell","0 = disabled","valid 0.0 - 1.0"], + sa_iflw_s: "line width", + sa_iflw_l: ["hatch line width","in millimeters"], + + sa_supp_m: "support", + sa_slyr_s: "base layers", + sa_slyr_l: ["base support layers","value range 0-10"], + sa_slgp_s: "gap layers", + sa_slgp_l: ["number of layers between","raft and bottom of object"], + sa_sldn_s: "density", + sa_sldn_l: ["used to compute the","number of support pillars","0.0-1.0 (0 = disable)"], + sa_slsz_s: "size", + sa_slsz_l: ["max size of a","support pillar","in millimeters"], + sa_slpt_s: "points", + sa_slpt_l: ["number of points in","each support pillar","in millimeters"], + sl_slen_l: "enable supports", + + sa_outp_m: "output", + sa_opzo_s: "przesunięcie osi Z", + sa_opzo_l: ["z layer offset","almost always 0.0","0.0-1.0 in millimeters"], + sa_opaa_s: "antyaliasing", + sa_opaa_l: ["włączenie tej opcji","tworzy większe pliki","ale pokazuje przejścia tonalne"] +}; diff --git a/web2/kiri/lang/pl.js b/web2/kiri/lang/pl.js new file mode 120000 index 00000000..73247290 --- /dev/null +++ b/web2/kiri/lang/pl.js @@ -0,0 +1 @@ +pl-pl.js \ No newline at end of file diff --git a/web2/kiri/lang/pt-pt.js b/web2/kiri/lang/pt-pt.js new file mode 120000 index 00000000..6a29b891 --- /dev/null +++ b/web2/kiri/lang/pt-pt.js @@ -0,0 +1 @@ +pt.js \ No newline at end of file diff --git a/web2/kiri/lang/pt.js b/web2/kiri/lang/pt.js new file mode 100644 index 00000000..21e152bf --- /dev/null +++ b/web2/kiri/lang/pt.js @@ -0,0 +1,650 @@ +// Portuguese localization for Kiri:Moto +// Will defer to English map for any missing key/value pairs +kiri.lang['pt'] = +kiri.lang['pt-pt'] = { + // common keys and menus + animate: "animar", // CAM animate button + arrange: "organizar", // layout workspace objects + axis: "eixo", // left object scale pop menu + clear: "limpar", // clear workspace (remove all objects) + copy: "copiar", + delete: "excluir", + detail: "detalhe", + done: "pronto", + enable: "permitir", + export: "exportar", + files: "arquivos", + help: "ajuda", + ghost: "fantasma", // left render pop menu (wireframe) + hide: "ocultar", // left render pop menu (invisible) + home: "início", + import: "importar", + language: "idioma", + machine: "máquinha", // device or machine + metric: "métrica", + name: "nome", + prefs: "preferências", // left menu "preferences" + preview: "pré-visualizar", + recent: "recente", + render: "renderizar", // left render pop menu + reset: "redefinir", + rotate: "girar", // left rotate pop menu + save: "salvar", + scale: "escalar", // left object scale pop menu + setup: "configurar", + settings: "configurações", + size: "tamanho", + slice: "fatiar", + solid: "sólido", // view type pop menu + start: "iniciar", + tool: "ferramenta", + tools: "ferramentas", // CAM tool menu button + top: "topo", + type: "tipo", // CAM tool type + version: "versão", + view: "visualizar", // left view pop menu + wire: "fio", // left render pop menu + + acct_xpo: ["faça um backup de seu aparelho","e perfis de dispositivos com a","opção para incluir objetos e posições","de espaço de trabalho"], + + // RIGHT-CLICK CONTEXT MENU + rc_clws: "limpar espaço de trabalho", + rc_xpws: "exportar espaço de trabalho", + rc_lafl: "apoiar sobre plano", + rc_mirr: "espelhar", + rc_dupl: "duplicar", + rc_xstl: "exportar como STL", + + // DEVICE MENU and related dialogs + dm_sldt: "selecione um tipo de dispositivo", + dm_stdd: "dispositivos padrão", + dm_mydd: "meus dispositivos", + dm_seld: "dispositivo selecionado", + dm_rcnt: "arquivos recentes", + dm_savs: "configurações salvas", + dm_appp: "Preferências da Aplicação", + + // CAM Tool Dialog + td_tyem: "extremo", // end mill + td_tybm: "bola", // ball mill + td_tytm: "cone", // taper mill + td_tonm: "ferramenta #", + td_shft: "haste", // endmill shaft specs + td_flut: "flauta", // endmill flute specs + td_tapr: "estreitar", // endmill taper specs + + // DEVICE dialog groups + dv_gr_dev: "dispositivo", + dv_gr_ext: "extrusora", + dv_gr_out: "saída", + dv_gr_gco: "macros gcode", + + // DEVICE dialog (_s = label, _l = hover help) + dv_name_s: "nome", + dv_name_l: "nome do dispositivo", + dv_fila_s: "filamento", + dv_fila_l: "diâmetro em milímetros", + dv_nozl_s: "nozzle", + dv_nozl_l: "diâmetro em milímetros", + dv_bedw_s: "largura", + dv_bedw_l: "unidades de espaço de trabalho", + dv_bedd_s: "profundidade", + dv_bedd_l: "unidades de espaço de trabalho", + dv_bedh_s: "altura", + dv_bedh_l: ["altura máxima de construção","em unidades de espaço de trabalho"], + dv_spmx_s: "fuso máximo", + dv_spmx_l: ["velocidade máxima de rotação do fuso","0 para desativar"], + dv_xtab_s: "posicionamento absoluto", + dv_xtab_l: "movimentos de extrusão absolutos", + dv_orgc_s: "centro de origem", + dv_orgc_l: "centro de origem da cama", + // dv_orgt_s: "origem topo", + // dv_orgt_l: "peça z origem topo", + dv_bedc_s: "cama circular", + dv_bedc_l: "a cama do dispositivo é circular", + dv_belt_s: "cama de cinto", + dv_belt_l: "cama de impressão contínua", + dv_retr_s: "retração do firmware", + dv_retr_l: ["fimware de dispositivo suporta G10/G11"], + dv_fanp_s: "potência do ventilador", + dv_fanp_l: "definir a potência do ventilador de resfriamento", + dv_prog_s: "progresso", + dv_prog_l: "saída em cada % de progresso", + dv_layr_s: "camada", + dv_layr_l: ["saída em cada","mudança de camada"], + dv_tksp_s: "espaçador de token", + dv_tksp_l: ["adicionar um espaço entre","parâmetros do eixo do gcode","G0X0Y0X0","vs","G0 X0 Y0 Z0"], + dv_strc_s: "eliminar comentários", + dv_strc_l: ["eliminar comentários de gcode","comentários começam com ;"], + dv_fext_s: "extensão do arquivo", + dv_fext_l: "extensão do nome do arquivo", + dv_dwll_s: "permanência", + dv_dwll_l: "script de permanência de gcode", + dv_tool_s: "mudança de ferramenta", + dv_tool_l: "script de mudança de ferramenta", + dv_sspd_s: "velocidade do fuso", + dv_sspd_l: "definir velocidade do fuso", + dv_paus_s: "pause", + dv_paus_l: "script gcode de pause", + dv_head_s: "cabeçalho", + dv_head_l: "script gcode de cabeçalho", + dv_foot_s: "rodapé", + dv_foot_l: "script gcode de rodapé", + dv_lzon_s: "laser ligado", + dv_lzon_l: "script gcode de laser ligado", + dv_lzof_s: "laser desligado", + dv_lzof_l: "script gcode de laser desligado", + dv_exts_s: "selecionar", + dv_exts_l: "gcode run para habilitar esta extrusora", + dv_dext_s: "desmarcar", + dv_dext_l: "gcode run antes de habilitar outra extrusora", + dv_extd_s: "desmarcar", + dv_extd_l: "gcode para desmarcar esta extrusora", + dv_exox_s: "compensação x", + dv_exox_l: "nozzle offset x", + dv_exoy_s: "compensação y", + dv_exoy_l: "nozzle offset y", + + // MODE + mo_menu: "modo", + mo_fdmp: "Impressão FDM", + mo_slap: "Impressão SLA", + mo_lazr: "Corte a Laser", + mo_cncm: "Fresadora CNC", + + // SETUP + su_menu: "configurar", + su_devi: "Dispositivos", + su_tool: "Ferramentas", + su_locl: "Local", + su_xprt: "Exportar", + su_help: "Ajuda", + + // LOAD + fe_menu: "arquivo", + fn_recn: "Recente", + fn_impo: "Importar", + + // FUNCTION + fn_menu: "ação", + fn_arra: "Organizar", + fn_slic: "Fatiar", + fn_prev: "Pré-visualizar", + fn_expo: "Exportar", + + // VIEW + vu_menu: "visualizar", + vu_home: "Início", + vu_rset: "Redefinir", + vu_sptp: "Topo", + vu_spfr: "Frente", + vu_splt: "Esquerda", + vu_sprt: "Direita", + + // WORKSPACE + ws_menu: "visualizar", + ws_save: "Salvar", + ws_cler: "Limpar", + + // OPTIONS + op_menu: "interface", + op_disp: "exibir", + op_xprt_s: "especialista", + op_xprt_l: "mostrar mais opções de configuração", + op_decl_s: "decalques", + op_decl_l: "exibir decalques e logotipos de dispositivos", + op_dang_s: "experimental", + op_dang_l: "mostrar parâmetros experimentais", + op_hopo_s: "pairar", + op_hopo_l: ["habilitar pairar o menu","para ativar"], + op_dark_s: "modo escuro", + op_dark_l: "interface modo escuro", + op_comp_s: "interface compacta", + op_comp_l: ["interface de usuário compacta","melhor para telas pequenas","e tablets"], + op_shor_s: "mostrar origem", + op_shor_l: "mostrar a origem do dispositivo ou processo", + op_shru_s: "mostrar réguas", + op_shru_l: ["mostrar réguas de eixos","nas principais linhas de grade"], + op_sped_s: "mostrar velocidades", + op_sped_l: ["mostrar velocidade para a barra de cores","em modo de visualização"], + op_auto_s: "layout automático", + op_auto_l: ["faz o layout automático da plataforma","quando novos itens são adicionados"], + op_free_s: "layout livre", + op_free_l: ["permite um layout arrastável","sem efeito no modo laser"], + op_spcr_s: "espaçamento", + op_spcr_l: ["espaçamento entre objetos","durante layout automático","em unidades de espaço de trabalho"], + op_orth_s: "ortográfico", + op_orth_l: ["exibição ortográfica","requer atualização de página"], + op_invr_s: "zoom invertido", + op_invr_l: ["zoom invertido","com roda do mouse"], + op_save_s: "salvar automaticamente", + op_save_l: ["preserva objetos no espaço de trabalho","entre as recargas de aplicação"], + op_line_s: "tipo de linha", + op_line_l: ["estilo de linha para renderização do caminho","impacta o desempenho 3d","caminho: 3d melhor","plano: 2d bom","linha = 1d rápido"], + op_unit_s: "unidades", + op_unit_l: ["unidades de espaço de trabalho afeta","velocidades e distâncias"], + op_anim_s: "animar", + op_anim_l: ["densidade da malha de animação","mais alto é mais denso","usa mais memória","e é mais lento"], + + lo_menu: "layout", + + pt_menu: "peças", + pt_deci_s: "decimar", + pt_deci_l: ["ativar ou desativar a decimação do ponto","durante a importação do port. para fatiamento mais rápido","e menor uso de memória"], + pt_qual_s: "qualidade", + pt_qual_l: ["nível de detalhe a ser retido","durante as operações de fatiamento","mais baixo é mais rápido"], + pt_heal_s: "cura malha", + pt_heal_l: ["tentar curar","malhas não múltiplas","extende o tempo de corte"], + + xp_menu: "exportações", + + // SETTINGS + se_menu: "perfil", + se_load: "carregar", + se_save: "salvar", + + // FDM SLICING + sl_menu: "layers", + sl_lahi_s: "altura", + sl_lahi_l: ["altura de cada fatia","camada em milímetros"], + ad_minl_s: "altura min", + ad_minl_l: ["altura adaptativa da camada min.","em milímetros","deve ser diferente de zero"], + sl_ltop_s: "camadas de topo", + sl_ltop_l: ["número de camadas sólidas","para fazer cumprir na","parte superior da impressão"], + sl_lsld_s: "camadas sólidas", + sl_lsld_l: ["áreas de preenchimento sólido computadas","de camadas de deltas. ver","menu pop camada"], + sl_lbot_s: "camadas de base", + sl_lbot_l: ["número de camadas sólidas","para fazer cumprir na","parte inferior da impressão"], + ad_adap_s: "adaptativo", + ad_adap_l: ["usar alturas de camadas adaptativas","com 'altura de camada' como máximo","e 'camada min' como o min"], + + // FDM SHELLS + sl_shel_s: "quantidade de cartuchos", + sl_shel_l: ["número de paredes","de perímetro para gerar"], + sl_ordr_s: "ordem de cartuchos", + sl_ordr_l: ["ordem de saída de cartuchos","dentro para fora","ou de fora para dentro","afeta a qualidade da superfície"], + sl_strt_s: "início da camada", + sl_strt_l: ["ponto inicial da camada","último = última camada final","centro = centro da peça","origem = origem do dispositivo"], + ad_thin_s: "paredes finas", + ad_thin_l: ["detecta e preenche lacunas","entre paredes de cartuchos"], + + // FDM FILL + fi_menu: "preenchimento", + fi_type: "tipo de preenchimento", + fi_pcnt_s: "fração de preenchimento", + fi_pcnt_l: ["fill density values","0.0 - 1.0"], + fi_angl_s: "início sólido", + fi_angl_l: ["ângulo inicial em graus","90 graus adicionados a ","cada camada seguinte","se aplica somente a camadas sólidas"], + fi_wdth_s: "largura sólida", + fi_wdth_l: ["line width for solid fill","as a fraction of nozzle width","values < 1 are more dense","good for surface finishes","0.0 - 1.0"], + fi_over_s: "sobreposição de cartucho", + fi_over_l: ["overlap with shell and other fill","as fraction of nozzle diameter","0.0 - 2.0"], + // fi_rate_s: "velocidade de impressão", + fi_rate_l: ["velocidade de extrusão para preenchimento","defina como 0 para usar padrão","velocidades de impressão de saída"], + + // FDM FIRST LAYER + fl_menu: "base", + fl_lahi_s: "altura da camada", + fl_lahi_l: ["altura de cada fatia","em milímetros","deve ser >= altura da fatia"], + fl_rate_s: "velocidade do cartucho", + fl_rate_l: ["velocidade máxima de impressão do cartucho","em milímetros / segundo"], + fl_frat_s: "velocidade de preenchimento", + fl_frat_l: ["velocidade máxima de impressão de preenchimento","em milímetros / segundo"], + fl_mult_s: "fator de fluxo", + fl_mult_l: ["multiplicador de extrusão","0.0 - 2.0"], + fl_sfac_s: "fator de largura", + fl_sfac_l: ["multiplier on nozzle size","changes line spacing"], + fl_skrt_s: "skirt count", + fl_skrt_l: ["number of first-layer offset","brims to generate"], + fl_skro_s: "skirt offset", + fl_skro_l: ["skirt offset from part","in millimeters"], + fl_nozl_s: "nozzle temp", + fl_nozl_l: ["graus em celsius","configuração de saída utilizada","quando este é zero"], + fl_bedd_s: "temperatura da cama", + fl_bedd_l: ["graus em celsius","configuração de saída utilizada","quando este é zero"], + fr_spac_s: "raft gap", + fr_spac_l: ["espaçamento adicional de camadas","entre a 1ª camada e raft","em milímetros"], + fr_nabl_s: "raft enable", + fr_nabl_l: ["create a raft under the","model for better adhesion","uses skirt offset and","disables skirt output"], + + // FDM BELT ONLY + fl_zoff_s: "compensação do cinto", + fl_zoff_l: ["deslocamento de altura do cinto","de menor extrusão","em milímetros"], + fl_brim_s: "tamanho da borda", + fl_brim_l: ["adiciona borda na parte inferior","o tamanho é a largura em milímetros","0 para desativar"], + fl_brmn_s: "gatilho de borda", + fl_brmn_l: ["adicione borda apenas quando o segmento","cinto de frente é mais curto do que este","valor em milímetros","0 = Infinito"], + fl_bled_s: "âncora parcial", + fl_bled_l: ["âncora da parte do cinto","no início da impressão","em milímetros"], + + // FDM SUPPORT + sp_menu: "suporte", + sp_detect: "detectar", + sp_dens_s: "densidade", + sp_dens_l: ["fração 0.0 - 1.0","recomendada 0.15","0 para desativar"], + sp_size_s: "tamanho do pilar", + sp_size_l: ["largura do pilar","em milímetros"], + sp_offs_s: "compensação da peça", + sp_offs_l: ["deslocamento da peça","em milímetros"], + sp_gaps_s: "camadas de lacuna", + sp_gaps_l: ["número de camadas","deslocamento da peça"], + sp_span_s: "intervalo máximo", + sp_span_l: ["extensão não suportada que causa","um novo suporte a ser gerado","em milímetros"], + sp_angl_s: "ângulo máximo", + sp_angl_l: ["ângulo máximo de projeção antes","de gerar o pilar de suporte"], + sp_area_s: "área mínima", + sp_area_l: ["área mínima para","uma coluna de suporte","em milímetros"], + sp_xpnd_s: "expandir", + sp_xpnd_l: ["expandir a área de suporte","além do limite parcial","em milímetros"], + sp_nozl_s: "extrusora", + sp_nozl_l: ["em sistemas multi-extrusora","a extrusora a ser usada como","material de suporte"], + sp_auto_s: "automático", + sp_auto_l: ["habilita suportes gerados","usando geometria em tempo de corte","os suportes só aparecerão","após a conclusão do corte"], + + // LASER SLICING + ls_offs_s: "compensação", + ls_offs_l: ["ajustar a largura do feixe","em milímetros"], + ls_lahi_s: "altura", + ls_lahi_l: ["altura da camada","em milímetros","0 = auto/detectar"], + ls_lahm_s: "mínimo", + ls_lahm_l: ["altura mínima da camada","irá mesclar fatias automáticas","sob esta espessura","em milímetros"], + ls_sngl_s: "único", + ls_sngl_l: ["executa apenas uma fatia","na altura da camada especificada"], + + // CNC COMMON terms + cc_tool: "ferramenta", + cc_offs_s: "compensação", + cc_offs_l: ["compensação do centro da ferramenta","do caminho escolhido"], + cc_spnd_s: "rpm do fuso", + cc_spnd_l: ["velocidade do fuso em","revoluções/minuto"], + cc_sovr_s: "aumentar", + cc_sovr_l: ["como uma fração de","diâmetro da ferramenta"], + cc_sdwn_s: "diminuir", + cc_sdwn_l: ["diminuir a profundidade","para cada passagem","em unidades de espaço de trabalho","0 para desativar"], + cc_feed_s: "velocidade de avanço", + cc_feed_l: ["velocidade máxima de corte em","unidades de espaço de trabalho/minuto"], + cc_plng_s: "taxa de imersão", + cc_plng_l: ["velocidade máxima do eixo z em","unidades de espaço de trabalho/minuto"], + cc_sngl_s: "selecione apenas linhas", + cc_sngl_l: ["selecionar apenas bordas individuais","em vez de polilinhas conectadas"], + + // CNC COMMON + cc_menu: "limites", + cc_flip: "virar", + cc_rapd_s: "avançar xy", + cc_rapd_l: ["velocidade máxima de movimentos xy","em unidades de espaço de trabalho/minuto"], + cc_rzpd_s: "avançar z", + cc_rzpd_l: ["velocidade máxima de movimentos z","em unidades de espaço de trabalho/minuto"], + + cc_loff_s: "compensação", + cc_loff_l: ["distância da superfície do material","para passar de nivelamento","em unidades de espaço de trabalho"], + + // CNC ROUGHING + cr_menu: "retificar", + cr_lsto_s: "deixar material", + cr_lsto_l: ["compensação horizontal das faces verticais","material para deixar para a passagem de acabamento","em unidades de espaço de trabalho"], + cr_ease_s: "suavizar", + cr_ease_l: ["cortes de imersão vão","baixar em espiral ou suavizar","seguindo um caminho linear"], + cr_clrt_s: "limpar o topo", + cr_clrt_l: ["faz uma passagem de limpeza","na área delimitadora da peça","em z = 0"], + cr_clrp_s: "limpar vazios", + cr_clrp_l: ["fresar através de cavidades","em vez de apenas o contorno"], + cr_clrf_s: "limpar faces", + cr_clrf_l: ["interpolar descer para","limpar todas as áreas planas detectadas"], + cr_olin_s: "apenas interior", + cr_olin_l: ["limitar corte para","dentro dos limites da peça"], + + // CNC OUTLINE + co_menu: "contorno", + co_dogb_s: "dogbones", + co_dogb_l: ["inserir cortes dogbones","dentro dos cantos"], + co_wide_s: "recorte largo", + co_wide_l: ["alargar caminhos recortados externos","para cortes profundos em material duro"], + co_olin_s: "apenas interior", + co_olin_l: ["limitar corte para","dentro dos limites da peça"], + co_olot_s: "apenas exterior", + co_olot_l: ["limitar corte para","exterior dos limites da peça","que pode ser pensado","como o contorno da sombra"], + co_omit_s: "omitir através", + co_omit_l: "elimine buracos passantes", + co_olen_s: "habilitar", + co_olen_l: "corte de contorno habilitado", + + // CNC CONTOUR + cn_menu: "contorno", + cf_angl_s: "ângulo máximo", + cf_angl_l: ["ângulos maiores que este","são considerados verticais"], + cf_curv_s: "apenas curvas", + cf_curv_l: ["limitar limpeza linear","para superfícies curvas"], + cf_olin_s: "apenas interior", + cf_olin_l: ["limitar corte para","dentro dos limites da peça"], + cf_linx_s: "habilitar passagem y", + cf_linx_l: "acabamento linear do eixo y", + cf_liny_s: "habilitar passagem x", + cf_liny_l: "acabamento linear do eixo x", + + // CNC TRACE + cu_menu: "traçar", + cu_type_s: "tipo", + cu_type_l: ["seguir = ponta da ferramenta segue a linha","direita ou esquerda = ponta da ferramenta","segue o deslocamento da linha pelo raio da ferramenta"], + + // CNC DRILLING + cd_menu: "furar", + cd_axis: "eixo", + cd_points: "pontos", + cd_plpr_s: "imersão por", + cd_plpr_l: ["imersão máxima entre","períodos de permanência","em unidades de espaço de trabalho","0 para desativar"], + cd_dwll_s: "tempo de permanência", + cd_dwll_l: ["tempo de permanência","entre imersões","em milissegundos"], + cd_lift_s: "elevação de perfuração", + cd_lift_l: ["elevação entre as imersões","após o período de permanência","em unidades de espaço de trabalho","0 para desativar"], + cd_regi_s: "registro", + cd_regi_l: ["buracos de registro de perfuração","for double-sided parts","independent of enable","drilling but uses same","tool and settings"], + + // CNC CUTOUT TABS + ct_menu: "tabs", + ct_angl_s: "ângulo", + ct_angl_l: ["ângulo inicial para espaçamento da guia","em graus (0-360)"], + ct_numb_s: "contagem", + ct_numb_l: ["número de guias para usar","será espaçado uniformemente","ao redor da peça"], + ct_wdth_s: "largura", + ct_wdth_l: "largura em unidades de espaço de trabalho", + ct_hght_s: "altura", + ct_hght_l: "altura em unidades de espaço de trabalho", + ct_dpth_s: "profundidade", + ct_dpth_l: ["distância em unidades de espaço de trabalho","que a guia projeta da","superfície da peça"], + ct_midl_s: "linha média", + ct_midl_l: ["use a linha média da guia","em vez de z inferior","para trabalho frente e verso"], + ct_nabl_s: "auto", + ct_nabl_l: ["geração automática de guias radiais","projetado do centro da peça","usando contagem e deslocamento de ângulo"], + + // OUTPUT + ou_menu: "saída", + + // LASER KNIFE + dk_menu: "faca", + dk_dpth_s: "profundidade de corte", + dk_dpth_l: ["profundidade de corte final","em milímetros"], + dk_pass_s: "passagens de corte", + dk_pass_l: ["número de passagens","até a profundidade do corte"], + dk_offs_s: "deslocamento da ponta", + dk_offs_l: ["distância da ponta da lâmina","para o centro da ferramenta","em milímetros"], + + // OUTPUT LASER + ou_spac_s: "espaçamento", + ou_spac_l: ["distância entre a saída da camada","em milímetros"], + ou_scal_s: "escala", + ou_scal_l: "multiplicador (0.1 a 100)", + ou_powr_s: "poder", + ou_powr_l: ["0 - 100","representa %"], + ou_sped_s: "velocidade", + ou_sped_l: "milímetros/segundo", + ou_mrgd_s: "mesclado", + ou_mrgd_l: ["mesclar todas as camadas usando","codificação de cores para denotar","profundidade de empilhamento"], + ou_grpd_s: "agrupado", + ou_grpd_l: ["reter cada camada como","um agrupamento unificado","em vez de polígonos","separados"], + ou_layr_s: "ordem de camadas", + ou_layr_l: ["ordem da camada de saída","do canto superior direito para","inferior esquerdo"], + ou_layo_s: "cor da camada", + ou_layo_l: ["cores da camada de saída","para cada índice z","substituído por mesclado"], + ou_drkn_s: "faca de arrasto", + ou_drkn_l: ["habilitar faca de arrasto","saída em gcode","raios de corte são adicionados","para os cantos com","passadas de corte"], + + // OUTPUT FDM + ou_nozl_s: "nozzle temp", + ou_nozl_l: "graus em celsius", + ou_bedd_s: "temperatura da cama", + ou_bedd_l: "graus em celsius", + ou_feed_s: "velocidade de impressão", + ou_feed_l: ["velocidade máxima de impressão","milímetros/segundo"], + ou_fini_s: "velocidade de acabamento", + ou_fini_l: ["velocidade do cartucho mais externo","milímetros/segundo"], + ou_move_s: "velocidade de movimento", + ou_move_l: ["velocidade de movimento sem impressão","milímetros/segundo","0 = habilitar movimentos G0"], + ou_shml_s: "fator de cartucho", + ou_flml_s: "fator sólido", + ou_spml_s: "fator de preenchimento", + ou_exml_l: ["multiplicador de extrusão","0.0 - 2.0"], + ou_fans_s: "velocidade do ventilador", + ou_fans_l: "0 - 255", + + // OUTPUT CAM + ou_toll_s: "tolerância", + ou_toll_l: ["precisão de superfície","em unidades de espaço de trabalho","mais baixo é mais lento e","usa mais memória","0 = baseado automaticamente","na preferência animada"], + ou_zanc_s: "âncora z", + ou_zanc_l: ["controla a posição da peça","quando o material Z excede a parte Z"], + ou_ztof_s: "compensação z", + ou_ztof_l: ["compensa âncora z","em unidades de espaço de trabalho","não tem efeito quando","a âncora está no meio"], + ou_zbot_s: "z inferior", + ou_zbot_l: ["deslocamento da parte inferior","para limitar a profundidade de corte","em unidades de espaço de trabalho"], + ou_zclr_s: "espaço livre z", + ou_zclr_l: ["deslocamento de viagem seguro","de cima da peça","em unidades de espaço de trabalho"], + ou_ztru_s: "através z", + ou_ztru_l: ["estender a passagem de recorte para baixo","em unidades de espaço de trabalho"], + ou_conv_s: "convencional", + ou_conv_l: ["direção de fresagem","desmarque para 'subir'"], + ou_depf_s: "profundidade primeiro", + ou_depf_l: ["otimizar cortes de bolso","com prioridade de profundidade"], + + // CAM STOCK + cs_menu: "material", + cs_wdth_s: "largura", + cs_wdth_l: ["largura (x) em unidades de espaço de trabalho","0 padrão para o tamanho da peça"], + cs_dpth_s: "profundidade", + cs_dpth_l: ["profundidade (y) em unidades de espaço de trabalho","0 padrão para o tamanho da peça"], + cs_hght_s: "altura", + cs_hght_l: ["altura (z) em unidades de espaço de trabalho","0 padrão para o tamanho da peça"], + cs_offs_s: "compensação", + cs_offs_l: ["use largura, profundidade e altura","como compensação do máximo","padrão para o tamanho da peça"], + cs_clip_s: "recortar para", + cs_clip_l: ["áspero e esboço","recortar caminhos de corte","ao material definido"], + cs_offe_s: "habilitar", + cs_offe_l: "habilitar material de fresagem", + + // ORIGIN (CAM & LASER) + or_bnds_s: "limites de origem", + or_bnds_l: ["origem é relativa ao","limite de todos os objetos"], + or_cntr_s: "oragem central", + or_cntr_l: "origem é referenciada do centro", + or_topp_s: "origem superior", + or_topp_l: "origem são referências do topo dos objetos", + + // FDM ADVANCED + ad_menu: "especialista", + ad_rdst_s: "retrair distância", + ad_rdst_l: ["quantidade para retrair o filamento","para movimentos longos. em milímetros"], + ad_rrat_s: "taxa de retração", + ad_rrat_l: ["velocidade do filamento","retração em mm/s"], + ad_rdwl_s: "ativar o descanso", + ad_wpln_s: "retrair varredura", + ad_wpln_l: ["movimento não imprimível","após retração","em milímetros"], + ad_rdwl_l: ["tempo entre reengajamento","do filamento e movimento","em milissegundos"], + ad_scst_s: "costa do cartucho", + ad_scst_l: ["fim não imprimível","dos cartuchos de perímetro","em milímetros"], + ad_msol_s: "sólido mínimo", + ad_msol_l: ["área mínima (mm^2)","necessária para se manter sólida","deve ser > 0.1"], + ad_mins_s: "velocidade mínima", + ad_mins_l: ["velocidade mínima","para segmentos curtos"], + ad_spol_s: "caminho curto", + ad_spol_l: ["polígonos mais curtos que este","terão sua velocidade de impressão","reduzida para velocidade mínima","em milímetros"], + ad_arct_s: "tolerância de arco", + ad_arct_l: ["converter linhas facetadas em arcos","tolerância de desvio do ponto central","ao combinar pontos de arco","considere valores em torno de 0,15","em milímetros","0 para desativar"], + ad_zhop_s: "z hop dist", + ad_zhop_l: ["quantidade para aumentar z","em movimentos de retração","em milímetros","0 para desativar"], + ad_abkl_s: "antiretrocesso", + ad_abkl_l: ["para melhor acabamento de superfície plana","use micromovimentos para cancelar","o retrocesso na saída da camada sólida","em milímetros","0 para desativar","se o seu firmware tiver M425","coloque isso no cabeçalho do gcode","e deixe como 0"], + ad_lret_s: "retrair camada", + ad_lret_l: ["forçar a retração do filamento","entre camadas"], + ad_play_s: "polir camadas", + ad_play_l: ["polir até o especificado","# de camadas de cada vez"], + ad_pspd_s: "velocidade de polimento", + ad_pspd_l: ["velocidade de polimento","em milímetros/segundo"], + + // CAM EXPERT + cx_fast_s: "pular sombra", + cx_fast_l: ["desabilitar detecção de saliências","pode ser mais rápido e usar menos","memória com modelos complexos","mas falha com saliências","tente habilitar se fatiar","travar durante o sombreamento"], + + // FDM GCODE + ag_menu: "gcode", + ag_nozl_s: "nozzle", + ag_nozl_l: "select output nozzle or head", + ag_peel_s: "protetor de desprendimento", + ag_peel_l: ["começando nesta posição z do cinto","periodicamente rolar a impressão e","voltar ao cinto para soltá-lo","e evitar deflexão de rolamento"], + ag_paws_s: "pausar camadas", + ag_paws_l: ["lista de camadas separadas por vírgulas","para injetar comandos de pausa antes"], + ag_loop_s: "repetir camadas", + ag_loop_l: ["intervalos de camadas para repetir no formato","primeira-última-contagem, primeira-última-contagem,...","contagem omitida = 1"], + + // SLA MENU + sa_menu: "corte", + sa_lahe_s: "altura da camada", + sa_lahe_l: ["altura da camada","em milímetros"], + sa_shel_s: "cartucho vazio", + sa_shel_l: ["espessura do cartucho em mm","usar múltiplas camadas de altura","use 0 para sólido (desabilitado)"], + sa_otop_s: "topo aberto", + sa_otop_l: ["se cartucho está habilitado","resulta em um topo aberto"], + sa_obas_s: "base aberta", + sa_obas_l: ["se cartucho está habilitado","resulta em uma base aberta","desabilitado com suportes"], + + sa_layr_m: "camadas", + sa_lton_s: "luz de tempo", + sa_lton_l: ["luz de camada ligada","tempo em segundos"], + sa_ltof_s: "light off time", + sa_ltof_l: ["luz de camada desligada","tempo em segundos"], + sa_pldi_s: "distância de casca", + sa_pldi_l: ["distância de casca","em milímetros"], + sa_pllr_s: "taxa de elevação de casca", + sa_pllr_l: ["velocidade de elevação da casca","em mm/seg"], + sa_pldr_s: "taxa de descasque", + sa_pldr_l: ["velocidade de descasque","em mm/seg"], + + sa_base_m: "base", + sa_balc_s: "contagem de camadas", + sa_balc_l: ["número de","camadas base"], + sa_bltn_l: ["luz da camada de base ligada","tempo em segundos"], + sa_bltf_l: ["luz da camada de base desligada","tempo em segundos"], + + sa_infl_m: "preenchimento", + sa_ifdn_s: "densidade", + sa_ifdn_l: ["porcentagem de preenchimento","requer cartucho","0 = desativado","válido 0.0 - 1.0"], + sa_iflw_s: "espessura da linha", + sa_iflw_l: ["largura da linha hachurada","em milímetros"], + + sa_supp_m: "suporte", + sa_slyr_s: "camadas base", + sa_slyr_l: ["camadas de suporte de base","faixa de valor 0-10"], + sa_slgp_s: "camadas de lacuna", + sa_slgp_l: ["number of layers between","raft and bottom of object"], + sa_sldn_s: "densidade", + sa_sldn_l: ["usado para calcular o","número de pilares de suporte","0.0-1.0 (0 = desativar)"], + sa_slsz_s: "tamanho", + sa_slsz_l: ["tamanho máximo de um","pilar de suporte","em milímetros"], + sa_slpt_s: "pontos", + sa_slpt_l: ["número de pontos em","cada pilar de suporte","em milímetros"], + sl_slen_l: "habilitar suporte", + + sa_outp_m: "saída", + sa_opzo_s: "compensação z", + sa_opzo_l: ["compensação da camada z","quase sempre 0.0","0.0-1.0 em milímetros"], + sa_opaa_s: "suavidade", + sa_opaa_l: ["habilita o anti-aliasing (suavidade)","produz arquivos maiores","pode desfocar detalhes"] +}; diff --git a/web2/kiri/lang/zh.js b/web2/kiri/lang/zh.js new file mode 100644 index 00000000..c801d1e4 --- /dev/null +++ b/web2/kiri/lang/zh.js @@ -0,0 +1,649 @@ +// 英文。其他语言地图将遵循英语内容 +// 映射缺少的键/值对 +kiri.lang['zh'] = { + // 通用键及菜单 + animate: "动画", // CAM动画按钮 + arrange: "排布", // 对工作区对象排版 + axis: "轴", // 左侧对象比例弹出菜单 + clear: "清除", // 清空工作区(移除所有对象) + copy: "复制", + delete: "删除", + detail: "详情", + done: "完成", + enable: "开启", + export: "导出", + files: "文件", + help: "帮助", + ghost: "透明", // 左侧渲染弹出菜单(线框) + hide: "隐藏", // 左侧渲染弹出菜单(不可见) + home: "主界面", + import: "导入", + language: "语言", + machine: "设备", // 设备或机器 + metric: "单位", + name: "名称", + prefs: "偏好", // 左侧菜单“偏好设置” + preview: "预览", + recent: "最近", + render: "渲染", // 左侧渲染弹出菜单 + reset: "重置", + rotate: "旋转", // 左侧旋转弹出菜单 + save: "保存", + scale: "比例", // 左侧对象比例弹出菜单 + setup: "设定", + settings: "设置", + size: "大小", + slice: "切片", + solid: "实心", // 浏览类型弹出菜单 + start: "开始", + tool: "工具", + tools: "工具栏", // CAM 工具菜单按钮 + top: "顶面", + type: "类型", // CAM 工具类型 + version: "版本", + view: "视图", // 左侧浏览弹出菜单 + wire: "线框", // 左侧渲染弹出菜单 + + acct_xpo: ["备份您的设备及","设备配置文件,","可选择包括工作区、","对象及位置。"], + + //右键内容菜单 + rc_clws: "清楚工作区", + rc_xpws: "导出工作区", + rc_lafl: "平铺", + rc_mirr: "镜像", + rc_dupl: "拷贝", + rc_xstl: "导出为STL", + + //设备菜单及相关对话框 + dm_sldt: "选择设备类型", + dm_stdd: "标准设备", + dm_mydd: "我的设备", + dm_seld: "所选设备", + dm_rcnt: "最近使用文件", + dm_savs: "预存设置", + dm_appp: "应用偏好设置", + + // CAM工具对话框 + td_tyem: "立", // 立铣刀 + td_tybm: "球", // 球刀 + td_tytm: "锥", // 锥度铣刀 + td_tonm: "工具#", + td_shft: "轴", // 立铣刀轴规格 + td_flut: "槽", // 立铣刀槽规格 + td_tapr: "锥", // 立铣刀锥规格 + + // 设备对话组 + dv_gr_dev: "设备", + dv_gr_ext: "喷头", + dv_gr_out: "输出", + dv_gr_gco: "gcode宏", + + // 设备对话 (_s = label, _l = hover help) + dv_name_s: "名称", + dv_name_l: "设备名称", + dv_fila_s: "线料", + dv_fila_l: "直径毫米数", + dv_nozl_s: "喷嘴", + dv_nozl_l: "直径毫米数", + dv_bedw_s: "宽度", + dv_bedw_l: "工作区单位", + dv_bedd_s: "深度", + dv_bedd_l: "工作区单位", + dv_bedh_s: "高度", + dv_bedh_l: ["最大建模高度","工作区单位数"], + dv_spmx_s: "最大转轴", + dv_spmx_l: ["最大转轴转速","0为禁用"], + dv_xtab_s: "绝对位置", + dv_xtab_l: "喷头以绝对位置运动", + dv_orgc_s: "原点", + dv_orgc_l: "机床原点", + // dv_orgt_s: "顶部原点", + // dv_orgt_l: "z部分顶部原点", + dv_bedc_s: "圆形机床", + dv_bedc_l: "设备机床为圆形", + dv_belt_s: "履带机床", + dv_belt_l: "持续打印机床", + dv_retr_s: "固件回抽", + dv_retr_l: ["设备固件支持G10/G11"], + dv_fanp_s: "风扇强度", + dv_fanp_l: "设定冷却风扇强度", + dv_prog_s: "进程", + dv_prog_l: "各进程输出 %", + dv_layr_s: "层", + dv_layr_l: ["在各层变换时","进行输出"], + dv_tksp_s: "符号间隔", + dv_tksp_l: ["在gcode轴参数间","添加空隙","G0X0Y0X0","vs","G0 X0 Y0 Z0"], + dv_strc_s: "删除注释", + dv_strc_l: ["删除gcode注释","注释以;开头"], + dv_fext_s: "文件扩展名", + dv_fext_l: "文件扩展名", + dv_dwll_s: "驻留", + dv_dwll_l: "gcode驻留脚本", + dv_tool_s: "更换工具", + dv_tool_l: "工具更换脚本", + dv_sspd_s: "转轴转速", + dv_sspd_l: "设定转轴转速", + dv_paus_s: "暂停", + dv_paus_l: "gcode 暂停脚本", + dv_head_s: "header", + dv_head_l: "gcode header脚本", + dv_foot_s: "footer", + dv_foot_l: "gcode footer脚本", + dv_lzon_s: "激光 启用", + dv_lzon_l: "gcode 激光启用脚本", + dv_lzof_s: "激光 禁用", + dv_lzof_l: "gcode 激光禁用脚本", + dv_exts_s: "选中", + dv_exts_l: "gcode 运行以启用改喷头", + dv_dext_s: "取消勾选", + dv_dext_l: "启用另一喷头前运行gcode", + dv_extd_s: "取消勾选", + dv_extd_l: "取消选中该喷头的gcode", + dv_exox_s: "偏置 x", + dv_exox_l: "喷嘴偏置 x", + dv_exoy_s: "偏置 y", + dv_exoy_l: "喷嘴偏置 y", + + // 模式 + mo_menu: "模式", + mo_fdmp: "FDM打印", + mo_slap: "SLA模式", + mo_lazr: "激光切割", + mo_cncm: "CNC铣刀", + + // 设置 + su_menu: "设置", + su_devi: "设备", + su_tool: "工具", + su_locl: "本地", + su_xprt: "导出", + su_help: "帮助", + + //载入 + fe_menu: "文件", + fn_recn: "最近文件", + fn_impo: "导入", + + // 功能 + fn_menu: "动作", + fn_arra: "排布", + fn_slic: "切片", + fn_prev: "预览", + fn_expo: "导出", + + // 视图 + vu_menu: "视图", + vu_home: "首页", + vu_rset: "重置", + vu_sptp: "顶面", + vu_spfr: "正面", + vu_splt: "左侧", + vu_sprt: "右侧", + + // 工作区 + ws_menu: "预览", + ws_save: "保存", + ws_cler: "清除", + + // 设定 + op_menu: "界面", + op_disp: "显示", + op_xprt_s: "专家模式", + op_xprt_l: "显示更多设定选项", + op_decl_s: "贴花", + op_decl_l: "显示设备贴花及logo", + op_dang_s: "实验", + op_dang_l: "显示实验参数", + op_hopo_s: "悬停弹出", + op_hopo_l: ["启用菜单悬停","激活"], + op_dark_s: "暗色模式", + op_dark_l: "暗色界面", + op_comp_s: "紧凑UI", + op_comp_l: ["紧凑的用户界面","适用于小屏幕","及平板"], + op_shor_s: "显示原点", + op_shor_l: "显示设备或进程原点", + op_shru_s: "显示标尺", + op_shru_l: ["在主要参考线上","显示轴标尺"], + op_sped_s: "显示速度", + op_sped_l: ["在预览模式下","以色码显示速度"], + op_auto_s: "自动排版", + op_auto_l: ["添加新项目时","自动进行排版"], + op_free_s: "自定义布局", + op_free_l: ["允许拖放布局","激光模式下不产生影响"], + op_spcr_s: "间距", + op_spcr_l: ["自动排版时","以工作区单位表示的","对象间距"], + op_orth_s: "正交", + op_orth_l: ["正交显示","需要刷新页面"], + op_invr_s: "反向缩放", + op_invr_l: ["反转鼠标滚轮","滚动缩放"], + op_save_s: "自动保存", + op_save_l: ["在应用重新加载之间","保存工作区内的对象"], + op_line_s: "线条类型", + op_line_l: ["路径渲染用的线条类型","影响3D性能","路径:3D最佳","平面:2D适用","线条 = 1D 最速"], + op_unit_s: "单位", + op_unit_l: ["工作区单位影响","速度与距离"], + op_anim_s: "动画", + op_anim_l: ["动画网格密度","高数值为高密度","占用更多内存","且更慢"], + + lo_menu: "布局", + + pt_menu: "部件", + pt_deci_s: "抽取", + pt_deci_l: ["在导入时启用或禁用点抽取","以加快切片速度","并减少耗用内存"], + pt_qual_s: "质量", + pt_qual_l: ["切片过程中","维持的细节程度","越低速度越快"], + pt_heal_s: "修复网格", + pt_heal_l: ["尝试修复","非流形网格","延长切片耗时"], + + xp_menu: "输出", + + //设定 + se_menu: "设定", + se_load: "载入", + se_save: "存储", + + // FDM 切片 + sl_menu: "层", + sl_lahi_s: "高度", + sl_lahi_l: ["各切片层高度","毫米数"], + ad_minl_s: "最小高度", + ad_minl_l: ["适应性最小层高","毫米数","不能为零"], + sl_ltop_s: "顶层", + sl_ltop_l: ["在打印顶部","执行的","固体层数"], + sl_lsld_s: "固体层", + sl_lsld_l: ["根据图层增量计算的","实心填充区域。见","层弹出菜单"], + sl_lbot_s: "底层", + sl_lbot_l: ["在打印底部","执行的","固体层数"], + ad_adap_s: "适应性", + ad_adap_l: ["使用适应性层高","其中‘层高’代表最大值","‘最小层’代表最小值"], + + // FDM 外壳 + sl_shel_s: "外壳数", + sl_shel_l: ["需生成的","围墙数"], + sl_ordr_s: "外壳顺序", + sl_ordr_l: ["输出外壳顺序","由内至外","或由外至内","影响表面质量"], + sl_strt_s: "层始点", + sl_strt_l: ["层的始发点","最终 = 最终层截止处","中心 = 部件中心","原点 = 设备原点"], + ad_thin_s: "薄壁", + ad_thin_l: ["检测并填补","外壳间隙"], + + // FDM 填充 + fi_menu: "填充", + fi_type: "填充类型", + fi_pcnt_s: "填充分数", + fi_pcnt_l: ["填充密度","0.0 - 1.0"], + fi_angl_s: "固体开始", + fi_angl_l: ["起始角度","给以下每一层","增加90度","仅适用于固体层"], + fi_wdth_s: "固体宽度", + fi_wdth_l: ["固体填充线宽","数值为喷头宽度的一部分","< 1时密度更高","适用于表面处理","0.0 - 1.0"], + fi_over_s: "外壳重叠", + fi_over_l: ["与外壳及其他填充重叠","数值为喷头直径的一部分","0.0 - 2.0"], + // fi_rate_s: "打印速度", + fi_rate_l: ["线料喷出速度","设为0以使用默认","输出打印速度"], + + // FDM 第一层 + fl_menu: "基座", + fl_lahi_s: "层高", + fl_lahi_l: ["每一切片厚度的","毫米数","应该≥切片厚度"], + fl_rate_s: "外壳速度", + fl_rate_l: ["外壳打印最大速度","以毫米/分钟表示"], + fl_frat_s: "填充速度", + fl_frat_l: ["填充打印最大速度","以毫米/分钟表示"], + fl_mult_s: "流量系数", + fl_mult_l: ["喷出倍数","0.0 - 2.0"], + fl_sfac_s: "宽度系数", + fl_sfac_l: ["喷头大小倍数","改变线的间隔"], + fl_skrt_s: "裙边数", + fl_skrt_l: ["需生成的首层偏置","边缘数"], + fl_skro_s: "裙边偏置", + fl_skro_l: ["距离部件的裙边偏置","毫米数"], + fl_nozl_s: "喷嘴加热温度", + fl_nozl_l: ["以摄氏度表示","该数值为0时","使用的输出设定"], + fl_bedd_s: "热床温度", + fl_bedd_l: ["以摄氏度表示","该数值为0时","使用的输出设定"], + fr_spac_s: "底座空隙", + fr_spac_l: ["第一层和底座之间的","额外空隙","毫米数"], + fr_nabl_s: "启用底座", + fr_nabl_l: ["在模型下方创建底座","以增强粘附性","使用裙边偏置并","禁用裙边输出"], + + // FDM 打印带专用 + fl_zoff_s: "打印带偏置", + fl_zoff_l: ["最低挤出","打印带偏置高度","毫米数"], + fl_brim_s: "边缘尺寸", + fl_brim_l: ["在部件底部增加裙边","尺寸为宽度毫米数","设为0以禁用"], + fl_brmn_s: "裙边触发", + fl_brmn_l: ["仅在面向打印带的区域比","该数值短时增加裙边","数值以毫米表示","0 = 无穷"], + fl_bled_s: "部件锚点", + fl_bled_l: ["开始打印时","打印带上的部件锚点","毫米数"], + + // FDM 支撑 + sp_menu: "支撑", + sp_detect: "检测", + sp_dens_s: "密度", + sp_dens_l: ["分数 0.0 - 1.0","建议值 0.15","0为禁用"], + sp_size_s: "柱子尺寸", + sp_size_l: ["柱宽","毫米数"], + sp_offs_s: "部件偏置", + sp_offs_l: ["部件偏置","毫米数"], + sp_gaps_s: "空隙层", + sp_gaps_l: ["从部件偏置的","层数"], + sp_span_s: "最大范围", + sp_span_l: ["触发生成新支撑柱的","不受支撑的范围距离","毫米数"], + sp_angl_s: "最大角度", + sp_angl_l: ["需要生成支撑柱前的","最大悬空角度"], + sp_area_s: "最小区域", + sp_area_l: ["支撑柱所需的","最小区域","毫米数"], + sp_xpnd_s: "扩张", + sp_xpnd_l: ["支撑区域从","部件边缘扩张的","毫米数"], + sp_nozl_s: "喷头", + sp_nozl_l: ["在多喷头系统中","喷出支撑材料的","喷头"], + sp_auto_s: "自动", + sp_auto_l: ["启用生成支撑","使用切片当时几何","支撑只会在","切片完成后生成"], + + // LASER 切片 + ls_offs_s: "偏置", + ls_offs_l: ["调整激光宽度","毫米数"], + ls_lahi_s: "高度", + ls_lahi_l: ["层高","毫米数","0 = 自动/测量"], + ls_lahm_s: "最小值", + ls_lahm_l: ["最小层高","将以该厚度","并入自动切片","(以毫米计算)"], + ls_sngl_s: "单层", + ls_sngl_l: ["仅以特定层高","切割一层"], + + // CNC 通用术语 + cc_tool: "工具", + cc_offs_s: "偏置", + cc_offs_l: ["将工具中心从","所选路径偏置"], + cc_spnd_s: "转轴转速", + cc_spnd_l: ["转轴速","以 转/分钟 表示"], + cc_sovr_s: "步过", + cc_sovr_l: ["工具直径的","一部分"], + cc_sdwn_s: "降压", + cc_sdwn_l: ["每次通过后","降低的深度","以工作区单位表示","设为0以禁用"], + cc_feed_s: "输入率", + cc_feed_l: ["最大切割速率","以 工作区单位/分钟 表示"], + cc_plng_s: "切入率", + cc_plng_l: ["最大z轴速度","以 工作区单位/分钟 表示"], + cc_sngl_s: "仅选取线条", + cc_sngl_l: ["仅选中单侧边","不选中相邻多边形"], + + // CNC 通用 + cc_menu: "限值", + cc_flip: "翻转", + cc_rapd_s: "xy输入", + cc_rapd_l: ["最大 xy 移动输入","以 工作区单位/分钟 表示"], + cc_rzpd_s: "z 输入", + cc_rzpd_l: ["最大 z 移动输入","以 工作区单位/分钟 表示"], + + cc_loff_s: "偏置", + cc_loff_l: ["实行水平通过所需","与库存面间的距离","以工作区单位表示"], + + // CNC 粗加工 + cr_menu: "粗加工", + cr_lsto_s: "库存面距离", + cr_lsto_l: ["为最终通过留下的","距离垂直库存面的水平偏置","以工作区单位表示"], + cr_ease_s: "渐缓", + cr_ease_l: ["切入切割将","螺旋下降或","沿线性路径渐缓"], + cr_clrt_s: "清理顶面", + cr_clrt_l: ["在部件的边界区域","进行一次清理","清理位置为 z = 0"], + cr_clrp_s: "清理缺口", + cr_clrp_l: ["将口袋内侧磨平","而不是仅清理轮廓"], + cr_clrf_s: "清理表面", + cr_clrf_l: ["内插降压以","清除所有检测到的平坦区域"], + cr_olin_s: "仅内部", + cr_olin_l: ["限制对内部边界","的切割"], + + // CNC OUTLINE + co_menu: "轮廓", + co_dogb_s: "狗骨头", + co_dogb_l: ["在内角插入","狗骨头切割"], + co_wide_s: "宽切除", + co_wide_l: ["加宽外部切割路径","以对硬材料进行深切割"], + co_olin_s: "仅内部", + co_olin_l: ["限制对内部边界","的切割"], + co_olot_s: "仅外部", + co_olot_l: ["限制对外部边界","的切割","此处可以视作","投影轮廓"], + co_omit_s: "略过", + co_omit_l: "消除孔洞", + co_olen_s: "启用", + co_olen_l: "启用轮廓切割", + + // CNC 轮廓 + cn_menu: "边界", + cf_angl_s: "最大角度", + cf_angl_l: ["比该数值更大的角度","将视为垂直"], + cf_curv_s: "仅弧面", + cf_curv_l: ["限制对弧面的","线性清理"], + cf_olin_s: "仅内部", + cf_olin_l: ["限制对内部边界","的切割"], + cf_linx_s: "启用 y 通过", + cf_linx_l: "线性y轴修整", + cf_liny_s: "启用 x 通过", + cf_liny_l: "线性x轴修整", + + // CNC 追踪 + cu_menu: "追踪", + cu_type_s: "类型", + cu_type_l: ["跟踪 = 工具头追踪线条","向右或向左 = 工具头","根据工具范围跟踪线条"], + + // CNC 钻孔 + cd_menu: "钻孔", + cd_axis: "轴", + cd_points: "点", + cd_plpr_s: "切入 每", + cd_plpr_l: ["两次驻留间的","最深切入","以工作区单位表示","设为0以禁用"], + cd_dwll_s: "驻留时间", + cd_dwll_l: ["两次切入铣削间","的驻留时间","以微秒表示"], + cd_lift_s: "钻头提升", + cd_lift_l: ["驻留时间后","两次切入间的提升距离","以工作区单位表示","设为0以禁用"], + cd_regi_s: "定位", + cd_regi_l: ["为双面部件","钻定位孔","独立于钻孔启用","但使用同样的","工具和设置"], + + // CNC CUTOUT TABS + ct_menu: "标签", + ct_angl_s: "角度", + ct_angl_l: ["标签间隔的起始角度","度数 (0-360)"], + ct_numb_s: "数量", + ct_numb_l: ["使用的标签数","将在部件周围","均匀分布"], + ct_wdth_s: "宽度", + ct_wdth_l: "以工作区单位表示的宽度", + ct_hght_s: "高度", + ct_hght_l: "以工作区单位表示的宽度", + ct_dpth_s: "深度", + ct_dpth_l: ["以工作区单位表示的","标签从部件表面","投射的深度"], + ct_midl_s: "中位线", + ct_midl_l: ["处理双面部件时","使用标签的中位线","而非z底部"], + ct_nabl_s: "自动", + ct_nabl_l: ["使用数量和角度偏置","自动生成从部件中心","投影的径向标签"], + + // 输出 + ou_menu: "输出", + + // 激光刀 + dk_menu: "刀", + dk_dpth_s: "切割深度", + dk_dpth_l: ["最终切割深度","毫米数"], + dk_pass_s: "切割次数", + dk_pass_l: ["实现切割深度的","切割次数"], + dk_offs_s: "刀尖偏置", + dk_offs_l: ["刀尖至工具中心","的距离","毫米数"], + + // 输出 激光 + ou_spac_s: "间距", + ou_spac_l: ["层输出间距","毫米数"], + ou_scal_s: "倍增", + ou_scal_l: "乘数 (0.1至100)", + ou_powr_s: "功率", + ou_powr_l: ["0 - 100","代表%"], + ou_sped_s: "速度", + ou_sped_l: "毫米/分钟", + ou_mrgd_s: "合并", + ou_mrgd_l: ["合并所有层","并用色码表示","层叠深度"], + ou_grpd_s: "组合", + ou_grpd_l: ["将各层保留为","统一分组","而不是独立的","多边形"], + ou_layr_s: "层顺序", + ou_layr_l: ["输出层顺序","从右上到","左下"], + ou_layo_s: "层色", + ou_layo_l: ["被合并覆盖后的","各Z系数","输出层的颜色"], + ou_drkn_s: "拖动激光刀", + ou_drkn_l: ["在gcode中启用","拖动输出","切割半径","藉由切入","加到边角上"], + + // 输出 FDM + ou_nozl_s: "喷嘴加热温度", + ou_nozl_l: "摄氏度", + ou_bedd_s: "热床温度", + ou_bedd_l: "摄氏度", + ou_feed_s: "打印速度", + ou_feed_l: ["最大打印速度","毫米/分钟"], + ou_fini_s: "完成速度", + ou_fini_l: ["最外壳速度","毫米/分钟"], + ou_move_s: "移动速度", + ou_move_l: ["不打印时的移动速度","毫米/分钟","0 = 启用G0动作"], + ou_shml_s: "外壳参数", + ou_flml_s: "实体参数", + ou_spml_s: "填充参数", + ou_exml_l: ["喷嘴乘数","0.0 - 2.0"], + ou_fans_s: "风扇速度", + ou_fans_l: "0 - 255", + + // 输出 CAM + ou_toll_s: "容错", + ou_toll_l: ["表面精确度","以工作区单位表示","数值越小速度越慢","且占用更多内存","0 = 基于动画偏好","自动计算"], + ou_zanc_s: "z 锚点", + ou_zanc_l: ["控制当库存Z超过部件Z时","的部件位置"], + ou_ztof_s: "z 偏置", + ou_ztof_l: ["以工作区单位表示的","Z锚点偏置","在锚点位于中央时","不产生影响"], + ou_zbot_s: "z 底部", + ou_zbot_l: ["用于限制切割深度的","距离部件底部的偏置","以工作区单位表示"], + ou_zclr_s: "z 安全区", + ou_zclr_l: ["从部件顶部的","安全移动区偏置","以工作区单位表示"], + ou_ztru_s: "z 穿透", + ou_ztru_l: ["向下延伸切口","以工作区单位表示"], + ou_conv_s: "传统", + ou_conv_l: ["铣削方向","取消勾选以设为‘爬升’"], + ou_depf_s: "深度优先", + ou_depf_l: ["用深度优先","优化腔铣"], + + // CAM STOCK + cs_menu: "库存", + cs_wdth_s: "宽度", + cs_wdth_l: ["以工作区单位表示的宽度 (x)","0 默认为部件尺寸"], + cs_dpth_s: "深度", + cs_dpth_l: ["以工作区单位表示的深度 (y)","0 默认为部件尺寸"], + cs_hght_s: "高度", + cs_hght_l: ["以工作区单位表示的高度 (z) ","0 默认为部件尺寸"], + cs_offs_s: "偏置", + cs_offs_l: ["用宽度、深度、高度","作为从平台最大部件","尺寸的偏置值"], + cs_clip_s: "夹到", + cs_clip_l: ["粗加工和轮廓","将切割路径夹到","定义的库存"], + cs_offe_s: "启用", + cs_offe_l: "启用铣削库存", + + // 零点 (CAM & 激光) + or_bnds_s: "零点边界", + or_bnds_l: ["零点相对于","所有对象的边界"], + or_cntr_s: "原始零点", + or_cntr_l: "以中心为零点", + or_topp_s: "原始顶部", + or_topp_l: "以对象顶部为原始顶部", + + // FDM 高级 + ad_menu: "专家", + ad_rdst_s: "回抽距离", + ad_rdst_l: ["长动作线料回抽量","以毫米表示"], + ad_rrat_s: "回抽速率", + ad_rrat_l: ["线料回抽速率","以 毫米/秒 表示"], + ad_rdwl_s: "启用驻留", + ad_wpln_s: "回抽扫动", + ad_wpln_l: ["回抽后的","非打印移动","毫米数"], + ad_rdwl_l: ["重新激活线料和动作","的间隔","毫秒数"], + ad_scst_s: "外壳滑过", + ad_scst_l: ["外围外壳","非打印端","毫米数"], + ad_msol_s: "最小固体", + ad_msol_l: ["维持固体所需的","最小区域(平方毫米)","必须>0.1"], + ad_mins_s: "最小速度", + ad_mins_l: ["短距离的","最小速度"], + ad_spol_s: "短路径", + ad_spol_l: ["比这更短的多边形","的打印速度将","降低到最小速度","以毫米表示"], + ad_arct_s: "拱形容忍", + ad_arct_l: ["将多面线转换为拱形","配对拱形点时的","中心漂移容忍度","采用约0.15的数值","数值以毫米表示","设为0以禁用"], + ad_zhop_s: "z 跳跃距离", + ad_zhop_l: ["回抽过程中","z的提升高度","毫米数","设为0以禁用"], + ad_abkl_s: "防反冲", + ad_abkl_l: ["为取得更好的平面效果","使用微动作以抵消","固体层输出中的反冲","以毫米表示","设为0以禁用","若您的固件包含M425","请将脚本置入gcode header","并将数值保留为0"], + ad_lret_s: "层回抽", + ad_lret_l: ["在层与层之间","强制回收线料"], + ad_play_s: "层抛光", + ad_play_l: ["一次性给指定的","#层抛光"], + ad_pspd_s: "抛光速度", + ad_pspd_l: ["抛光速度","以毫米/分钟表示"], + + // CAM 专家 + cx_fast_s: "跳过阴影", + cx_fast_l: ["禁用悬挂检测","可提升速度并减少","复杂模型的内存占用","但不与悬挂兼容","如果上阴影时切片悬浮","请尝试启用"], + + // FDM GCODE + ag_menu: "gcode", + ag_nozl_s: "喷头", + ag_nozl_l: "选择输出喷口或喷头", + ag_peel_s: "防翘边", + ag_peel_l: ["从打印带Z位置开始","阶段性将打印物从打印带上取下","然后放回以去除黏连","并防止滚动瑕疵"], + ag_paws_s: "暂停层", + ag_paws_l: ["用逗号隔开的层","在各层前添加暂停指令"], + ag_loop_s: "循环层", + ag_loop_l: ["使用以下格式的层重复范围","首层-末层-次数,首层-末层-次数,……","忽略次数 = 1"], + + // SLA 菜单 + sa_menu: "切片", + sa_lahe_s: "层高", + sa_lahe_l: ["层高","毫米数"], + sa_shel_s: "空壳", + sa_shel_l: ["壳厚毫米数","为层高倍数","0为实心(禁用)"], + sa_otop_s: "打开顶部", + sa_otop_l: ["如果外壳已启用","则形成开放顶部"], + sa_obas_s: "打开基座", + sa_obas_l: ["若外壳已启用","则形成开放基座","支撑将禁用该功能"], + + sa_layr_m: "层", + sa_lton_s: "曝光时间", + sa_lton_l: ["层曝光","秒数"], + sa_ltof_s: "灯灭时间", + sa_ltof_l: ["层灯灭","秒数"], + sa_pldi_s: "剥离距离", + sa_pldi_l: ["剥离距离","毫米数"], + sa_pllr_s: "剥离提升速率", + sa_pllr_l: ["剥离提升速率","以 毫米/秒 表示"], + sa_pldr_s: "剥离跌落速度", + sa_pldr_l: ["剥离跌落速度","以 毫米/秒 表示"], + + sa_base_m: "基座", + sa_balc_s: "层数", + sa_balc_l: ["底层","层数"], + sa_bltn_l: ["底层曝光","秒数"], + sa_bltf_l: ["底层灯灭","秒数"], + + sa_infl_m: "填充", + sa_ifdn_s: "密度", + sa_ifdn_l: ["要求外壳的","填充百分比","0 = 禁用","有效范围 0.0 - 1.0"], + sa_iflw_s: "线宽", + sa_iflw_l: ["剖面线宽度","毫米数"], + + sa_supp_m: "支撑", + sa_slyr_s: "基座层", + sa_slyr_l: ["基座支撑层","数值范围0-10"], + sa_slgp_s: "空隙层", + sa_slgp_l: ["支架和物品底部","间的层数"], + sa_sldn_s: "密度", + sa_sldn_l: ["用于计算","支撑柱的数量","0.0-1.0 (0 = 禁用)"], + sa_slsz_s: "尺寸", + sa_slsz_l: ["支撑柱的","最大尺寸","毫米数"], + sa_slpt_s: "点", + sa_slpt_l: ["各支撑柱","内的点数","以毫米表示"], + sl_slen_l: "启用支撑", + + sa_outp_m: "输出", + sa_opzo_s: "z 偏置", + sa_opzo_l: ["z 层偏置","一般始终保持在0.0","0.0-1.0 毫米"], + sa_opaa_s: "抗锯齿", + sa_opaa_l: ["启动抗锯齿","文件体积增加","可能模糊细节"] +}; diff --git a/web2/kiri/logo-cube-128.png b/web2/kiri/logo-cube-128.png new file mode 100644 index 0000000000000000000000000000000000000000..be70182b3eb83e7c070eecdae8fa09538e18eef8 GIT binary patch literal 22399 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 LNkvXXu0mjfqgD5q literal 0 HcmV?d00001 diff --git a/web2/kiri/logo-cube-144.png b/web2/kiri/logo-cube-144.png new file mode 100644 index 0000000000000000000000000000000000000000..08fdd7b4f50a66462c23bd15ff0be231b9d1c3f6 GIT binary patch literal 25380 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 bB?bO}Vz6s5c>|#O00000NkvXXu0mjfolE$p literal 0 HcmV?d00001 diff --git a/web2/kiri/logo-cube-256.png b/web2/kiri/logo-cube-256.png new file mode 100644 index 0000000000000000000000000000000000000000..5ca332f2a0722f85707c0d154193fde8bb30db2b GIT binary patch literal 48110 zcmZ^}19W9gvp;-JY)&$9PHfwp*fu7%ZF6GJ#I`xH?U~rNtuN1e-;3|A``>Hr)wQeZ z*Im_Jr*?In9ibp6jtGYX2LJ#NB_%|a007`82?W4EeKtCdC8huXT)Tych=Qbu2uQ)v z&eX!%1OSkTNKS=TNL<1kx_BrI3&w=`CUuBUHCwm3;rT_8r~8}6B!Oe z3Eu{mw^{Z@6aITo6HJN01}G}9jUH3<+n!H1N&Cb8S-ZzaI?vPI_>5D}sL%46Ro*ztyZZBI?&ch&`>3Q|EF(Z^F(C(~B540O|{*z{lF zjCR2SKz|A*EHEShDJxpB2{V@jegc258ZTyQm_0}&=Cl=x_U{W#1;AI7yZw$=O8+2e+kG{)3hD1DuTWS~Fe?i|WeeOuBki0TF{ zC3KuAl`T=APjcITn9CHdV`o2Y;1V{&$U&;$0FMRYX3Fu~Z8#TZL73!8eMsll?6b9J zO+(mxp_1J0^UMJ!1r@Pp3`?t`^npei8Ubte4&4v?785Z4kqjyzVHNM zuzjzn`1|hfFOO0??GhIJ`nzP6Bl3vJ`}N9ig${8M!T`V=Z{0w6F_ zAk!ug(*PVO0XPC-gaZ&C0rV(f{MSE#aXUoc&7z9JF69oYQSG@Ti8Cl1qZV1YV=XM? z>TBzopbehoCg1liN?F_x^VZX1O`HLnD*g~$H%y@f0JAJ`!6#0_Ij=ChR9(QY6T?Wz z^!nN(BY4JP*;H^TK}2&Pp8*1`KVl6ueK+`Hpdd=13>v%|kepc_hglL+1DZX^TLH#3 zpd$xQB|szx2@je=h=voMKCpa~{1EuiO#%Y;ZDL@76ZVio2yuoZD1z7|aGCwbLAb-H zjzQkJ29yYqA=^Yq{XzZH+~)8qFc>0x3T!IKZ(x}L^ui0ddWS$(m>x|>b-cJ;FAG2o0<05I^cvs+M7|AvX2C`0s1{Vl>P}qii+Hn@JS@2 zxEO(vB4l|GC5R&ZHR;lY(}F7lNtz zC)0F?V~0G4Lbq7YJfYDu-D!run4D!0LW4$nca5ZF{jXwtmW49CT3F{KA^4DoA# ziyGFHcCG)^$!VjgQLFDKh z=wsnO`f2)y`X88?ncJA`ym@{Ovr;!5S;vU^ed zO53td89wQ^oM()?1^6V)7Fq*(N)3C*9uY*1J{1xfqsl>|AKExO8Cn_d8Qv3b8L6!L zR*H?h4GE1>4U<+sER`G98d{ghmcKUW&n8)9HK=G|pE29Wh>K7NidQIC&Q-FMS{2}y z{w!Z9coB^)IVedjQO{c_=O0M^bL9!0S^qw7fR^i526o(6(Q*}@3RT53?&gO z6LZeT%ZJJz8ZiLeHw>8@q0#j1gzr9>A*=R~JXw?R9vkJW7d&|4L#KX1-$j;u^n1e4iH-b-to_nzCq9f70+`dSSp&W7k0FGVK=b zR^p6hui}W{vbyCy0lTxi*D!NbF=uAxWfp%UdC;XptD~e7(-uw3t~IXb?S2(kUPaT_ zxU9M@<`m`jG9tTOwKHj7?clecd(d;uxqa0;*Uz$alXig1K*JEAUcV6BT>s#Cfkc=T z+a(_$-&~z%S76tAp)8;)5ThTok=B*@q4g01kP%eqKKBQi_Yl{fN@EOYwCo1GmM^Edg8m5NOmuuYfj&ob< zYN&IPdAmH$T*jj6p?*V+CH_G&5TB{&s7RA{oKK$TlNXgw#sqKlcW5@Az!L;>ppK*0sWHnf;q_a_a(=G{TSz0-~tro|7d$GjTM5#nQr;gUW)#ef4i0TMB z6%kb9ZT3&%D!PghTdjp*#DRAWxF(+Mne2-p}HqAe-IpAxW4 z0?qw{+m+irGR@vG3d9t?zb~+Ojw}jqYzaRysyG30Oz0&t?^;H&fY}mw)^X^aX)Z@J39i!~9DJCj*m(lyq1w*Tt-)lU7x z_F>Is0B(S5K*THM)PgEE`Ad@b;&0E3yV-ArsxRR${V${@Jtj6UcMliKAC0`FuX-Ws z-;@i*_U=a`z$?Igp!ty4)hyI7)O?;IUgq8`KaNvy`Z8uk7Ddv-vIt|iSMTa~rtZY& zW4{nQsg0E<&xicct#T`=7ASLQC;{4{a6}VmJ5(IBOkFjJMGYR4TPknC+6#Mc?=f6; z-DV?jFL{h!sq9z;%_UDH?FV9(n8=Q4SSghk&^1*)$ZV!hx~AUm>(lvF@|8O^ZU4G% z&AaKp@i-`^-_^mo*mP^}9B$>DZ*9AoDNK`gV_^8b>O*!o+F0(Z&fwzVs@t;fd3Ne@ zaAIc5xo+NJ;a9(xvR1jjaq4Sz-gIHxdS?H-a;UVp!_DF1iT|N9%FX*+r$f5S>t=n{ ztK%*H?EzX1LE^mrJeF6&=knfgzi&kRCxH>cSKfF(rRPtjCoaglI3!QW4xoIX?D*jQ z?#;uwZjX_-2=&8mvJ$gXJEwgW1-`R5y3X4*9PC}E_EkStlj|RRlwO+c;qGu0e_TJF z{M8s=ywvVlDRtvnFYl;+t2q=oa6f#Ui?tx6pn&5q^(lKfeQZ6EzD@m_sHyLuKh~M< zZS_HL`*x+WJpUz|$Y5Lp_@!>MUlg$Z2TU;LTRZr6c|=`(mWSAM#Z)%Yr5o=_xmhr0 zsunVUm;;Cs?Vc1g%)H2YQdvx=91QJmV^esNjYht_JpPxMXcGO#!t%I-9MIwl=!7jQ zB1e{y_2G$TeF6B#L~B;_)6rf-DO(qOkEA=9Y=r(O^#dsN!C7dR@8^G}v%w}BlBTk< z0P0T~1^@+w0)YQ3*9H4bc+vh%e`dcx9026M_yB+!5cj`mJs{u7Astt2Y;Z}{gEAF;W!vpqKhgPWThy&DU?oue58 z6Bid310yp7Gc(<%1f7$It+Sy!ovjnee+=@U(X^ zr+WWyiT^RyKP$_vVBv0Jts!b*V`BTSb>w1Y^f~KvGml#T|Is0sBjLNR`4nwb`X>{Xx~TaXx`| z>6(})=4{b|Y>Ne!$XJ4O5SWO9MgipCCt=wP4v@qp_YQu7!h#X_j!Qvg42%+#kU>L3 zXEPXTqV-t#Zqrfr^{(?_<^1jaqfGrR#rI`aYn7~{Q+rKIF4O7wmvZsspN5Is&>(*s ztHD~M3}u(sVblr36=+$P4v*E~ZgPk!CK&%i7sEeCgT>mn$*e2_ zk+l_GTVVrqFm)z~Z8I;|z47$H$#>rcJN=fN>`wb!$D`M$HGMv>d%CrmHBzU=B)B{x zN1LpZ1=HsB#g1*6^_BAUwY7}O6Rq&+nlsy>RwstHB#UPO>y>vmRrQsaWDHq&2-?bu z@xfX(ZQV-m5k~H=Lr2|*=7|YEexi-JaFRHj)3~Y=_6wULD`>Ev2%F2;(kQK_cmh@F z&Ff{5D$P*P7pnv?I*;G_(q15|7dBu+bLi+3h{AbQpq*FPg+o!N`3NSQfAcM$TF-e? zs`zNkWxSV_pLk7`)lp*L@q176J0I2Ox~}hop~*ozL1D} z;%PVg-sxsEU82hV^ix+=?bgwRwcyW2753dn4<=l3(d|QsJrEjWwK3C7FQQ{m7h*Pd zpJdc}FU#AeKwg;lwv4&RL0E)oyE+L`$+3}wto3g%t76CQR%O$PA6#}tHhsxTTY4W) zZ+7O+?0X0kp6k+3>{;@~sOO`cXQxDsJ7i%!$7V+>2+UDb%EOi`R@T;708j)%z#-?ZvAb#*@~??#BI2&>#`0!)G**{Gj)Yw-FPoxdk*2f*2V$e9=Gwx` ziT!K>gG{2?Oq-k52cC8AXMi$ok@`>=T)bRssxR=@QrKPwRoRGifw_T*8S8=BohuhzbIAfhDjTGYu)?P1aBQT_p)eKNi<~P@Xta(J*A_ z4;(%>T6QHyfY(VQY4F2{&`nB6md_6Z6<5ao=O|F}3zjX_=Z5 zFf%kzm>MR;k_dw}YyK{j!K1^?x3cpcSxgfQF{8$c^Zr5rbJdwq3UY3nf5jLu5MtLm zi#~CGF!?s0S>zFaa(_`>hC~1G^oqlKN2~3obveHTSmQTTzG<=ep3+QIDFIC)FB!tl zq1{n&!t>E{WbM&VxL5t5i-BaiIo%*zD%fL068jUuASh=78+noiw5tcd-8vm)X8K-W z>3Xjn#%gm-LccIRNK0efQs$Ne&;_O~0qN_k>4YLx&XC|_J~y{Xejj$ZY(N^FXt^I zfa~DGjI(@>oyB|Dh0V>N3)qlB0>+7>?2?ie!Muf6Upn3u{gIScUSPt_i4wK{3<{DM zds+bOT@JQuz&?eG4^0Xh;WLHq#);XL|5MS5!=!zT&!<23_mb?;aIN@J4Dz{|mzarD z-d+&SH%LFECL`L0d?Q5Sp`8WhXT>aKA@TgWWLKt!5pBhf(v1k6f#Cd$sHHZhPFwy~ z?RM&Z`peT#nOY%k`0P{Hx?t^gFZxO?aPY8ola(Fh!)HqN9fWjg>GDM}>Ds>$OHxB< zUo)TCmG4nDF-_3zZ!H$%4*j>{Y=?;YsUOY4j2=in!}-o{%SMB_9!jsn>mRaEalloH z-K+Cfv#EErpM6)OZ%ur+x!2c6nV|PeQ#{)am)@MO-a#k#*Z}q&T2W<@7JM3LlCiQ- zt;RP5m0$+QpqH-YKYv*8=#gyEb!^vkq{boIe{J1sOSC{cRq)n#(M^ z^H-f*)*$(NTwbQhWt6*FKg2bpKDWpg+ND{WMOy0D^7!~)Rd<|sBTAKr!x;Px{=DOE z_kXs^fRxLY*b2{J-uh!&q+_v#7~gd=>&>~Ns>?fjjP2d9+3Q*?%m{RvRUT&MWu@F4 z8)S6F5Ygt$2-n*~NooxlAjo$w)zE33fghW`&(k$Cd(8yF6!29n*3^5S4BjYSjR^7=c+RO#P2bN8)YAuE=u+~8r{44^$Ma#ivERv&6u zW@)KFdD_pH8KE$vZ4#uR%b$E%LgV-Bzf=yhPGIZ?a~19Q4wHPcw?bQnc-pJ! z-4XP!Tka(?FUggiHCuM&1XMdYzorvE&3D*eS$SwOT$H*!;oOw<>!-iG1qgr*<<_VVKBxZ2`?)m*IRZ~cDIIw^)~7yum{+3 z5VNMP%BD#OBSS<}n2j6TI)`!>? zctY;nafBOMds2IU|8WyZ9Y`p$)b&xkmG?RrrFt1Eqz($QpZejj$mR8yPigsP-M}72 z+FitshT$UCanS*sU3gsGJ>folm^bdrtm(>l95e((Pl?PPZqTzv&Ztip|^3r|(oBfoH zxS%Y~9pmOGABR^yf!X_9F=9*YaRzvk*`!iL0nkMsAKeg(DHgomHJX~{nPB5bRglTx z#!jQBIPb2DipMcQ?8*612i6W&j24dl!Y3copt5{3+*l~CC9N;9Gv$9P1Qx*fu8UkN7*7#B%Yuk zXur|AXF~sC0z6lGoUyU!9K4Na{?$O99?4JaHZ#`7-&d3~wXKg36 z6A+0LJ~5^cB(`GLVffKfN3PbIY9|6Ku5bDJPotl&0(s=V%$}c{II#xKvP=Nq`Rla{ z%=6Yug2Y26-dAm#jQi!co=u4Dqs%o6torY;@`H0hr4l>VQ?--$Uw+kY9#nU{w*`=N zEQE8HDwBICrP|x@VI&BP>DD#!dC?;3GbPM^6T2WC6}<}jl_4GB%LtBL4$w{vAY?PE zO?ot$Yu~fe(iVV+6(W~{l;@p6Z%f(6G4(Gidr?1Uitn3jb-Lg($VlZ}>1uBCT&-_! z?_VO=&Q2U8Li6o;p2W`n=`)#ChzR;P`p_~#B5hqYvnf@A2*OkWXqT7xk6{cq+1KQ$ zbTd#q^;699hFfuR{9b^D&bJG|=AHd}Ur0y=2Y45JZ(%3K!ia|XV$XgILoK#~oHm2R zLqfvmk~>ZF02yTF+gf0Zb9F1$Z*8aPyyy^c2!D|^mLBEUe_By=_wX6(gIM3Wkzr~n z=hZn2rt;~i^d`L7{&onr( zh?X4~r-Lza_tmX`AJ-aOQgqJzxm(%pD@BFQFL$G2LP%jV*T150UgUCTs)JM z@a<hVb01r>qR4IUdOa;iiM8*4nP--i291ouZYlvtO3ir22aVGYD1N^TQijG(!_--gTryFW&rxT2iuk=CL z8PCF%YZYKn1)8|)xVvn-z9=BSH9k{E$jJ>*63kWgH*UP{Tug?!>+ZVWnomwvlklRO z!V-JS_wliNdueXp6RN#>v5Ycs{$`|`N7mlcqu;sSG}UKl_6&}JVTJ?Nqq5|b|j21Wxqw!0n!ZB)Y`Fk`FTaZ!WZR4-&nb>a*-!w%k609X5 zS>z1js@#0EWBoI-KJzzE#+JRPm1e&)RmhogL#ivOe(Ja3`17db)PuR}^XYW{`YeAdS6T*W(O~=)*W3==;+5hcr~@IEb_h<^;1nS=4GoDdgf!&%OWix^h0Y(oa{HoK zmuXgnb4;9JeP}MHp>PV1h27pn^e# zr6W>9fH^lMsfyafV-%_MZ>DqG+p&H?;`gwQ^@tN0C$auH5iCC1Er;B*(Wqi^W0SG{ zebyiBfY6Gv%4CQfDDJg*@SM)eVyK|K1b=s9C5S1jV4R@IQt(=4C(Z=2FAoeW-@dLb zFNbIIV++Zk;Uhjs^T9mG>lX2#A$?^SC0oekLFH)iB~b&LZKPgu`V3SngWndN#>zr& zm+h72%DP$_8g8t-9(+Bb?Z8}v5o*Husd1|bYOWEbBT_CmYn)Q&xI$rgST4#dcrSEy z8+!@6oqL(`Ymj(C&M8r|eLr--@CLqKgvV7@I$}nb!Yiox@AHTn6AVna6B`U(^6l@$ z_KUKC|3-%d-*oU^ZTYSfvy@s!eUv%$gZo$dQ-H!B`5ARcl>1o8yPrPMZM!5669-$+ zWq8ahHwSD#J_Fhr+F#8lu$oIm`8InVy%*7pp>O$H2`0&J9V=_^5>_X%z7-h|oWP$3 z14w>cQfmzR@gRCj;o`q^Te7luD&ym?H&fVytKxiFM=QKpl1!h+CKoSXw3BO`yKSH_ zgiKO+z6wyR1tNTrWnv-$c3OlkyZ`yoi0e}CORzjtwCtmL1k|C9 zQ8QK5S&) zkU|TMgG(c3*JwVH_zDTZRLs!HNf%aQQpwTU+CVB?LXK1(WC^ zc0by>aONYaT8h_sy|vi!=_y|v2chuR@1UU7?`5zDNoCX^vN*o4-DR$Emsf@gSIQ8D zrXp?lH%rNsz|FZ$tLLsd$jhwn5H%@L2c>C}b-*2-q^4nW`Rvfj4!OA!s9hzsI&Sv! z&c|~iF)nj|A!4-vbW3Bt?-)tGHWsDFwR~PN13n1f7z5o4%Rmzokn+lE(ZHT8OIZPX zLEnS-#aifJcXtf0GL)=0OmjM)+L9d#v(xPht=S8nP@P*>hf|y6-?&SN5*|Wx49Rr3 zlo!RqD`CHL6(S~bg%lJP$MTt4;DN}>M6)p;?IuB!)I*%=*cuxKbdHEf7vD>Ik6$wx`N+jaaqd$wNFD7NEKgq6$}lp+^G* z=W%=uRxpqs?m9kJppkCvjc{Ki?))v8uD{YpVD4(;;S4!4enE{l`KPk+=uN?2a3W1e zP4jcZbcM!B+Fj5JF2rM@GLClR{@|Jaq}Y6YfkOIlvB<&}Yzx-1{KUolAn8~X64 z{;|$I+a>0QE*FA-M=>HO11Q>;WMe8Ix&i6|3CGr;E6C=E=BEW8JKg@CIq;UdJLrdn zC42^}iFU{O*5%arON0-Gfoxers)6yHEGP9EzDf6bKrrXJ`gfZiwyFNKx*$=?-|Hii zR;fwYA51ZX3o-}@)^m=77bm~BDr@F^S1*O3m~jUW zm&6g9et)6N)7~6;oUYsl!Hv z752o{w|W*`qd^5TR4-ft7L1u+QC)RV4Y4tAD06fR#TPp`uOE;6e}HQ=!bw^b))(}@ zX4luJ`W9DLpAyQ{zr(k!KM)R66o4IBq-A@?p@YZ0Phx%5W$nkoT{+OzBj`jHxnhbv z7_X#HB^2Ys{VNBJ4IaXZ_THAOiM!|`U-gO$r#J#Axk&}1l59Wr^+s0FPxh^*eQvMz zow*>Tp-7-0$ikHci+@(#AhwbAE_yBq*JTnkVJz?z8T$i7et0ihG^e3@EG z`f={NO^>-tp9ah^5G09;V;9C!!9Xdvad z1JOYF3lXIfZ^85J-daqtBdmkd&`=0K?5Hb)Pyz=seuSERA*MH^AQ;|Wmb1Jk-$uj6 z*e=tffgsOU2<0o>5TClI7Kptv5f#|s8dKR5%lZbx>}a<$`L~(EW39}u>?4>cRq04x zh`c)+?Mziq6=y?nP^?U5!8Jh%*i^sbk7!PBkdy+ozUxfiWnnw{YQ5oqGS+6pyF637 zEY1l~OA@l67+y4x2%MyxF^YxU`O57kx3aNu@me;7?%mPGeM$9b$}usu(vyq2L;amm*CkyA{>vKBG z@|izkW(2L&VY)C02~;NvS$f58Rgt4lwu6}hoD)r*Vpwr@Uk}IM`sZ1`I zC!5}q73+SKTb%XW*I{B3mR(v7pDZu0u3cVECivmOO74%Py-Pw08@-B4iTd;f9A%JF zFgUkI;`LW%8Z(h+Itwvh$%(Nzu>ksseA4`l=+t#fNgrm*P2Z3boaD3@?QH%`tJ`?gY(5gNRcH zE!dpqX7ODr2ERSGm6b5T*K-WfDK?q89sA!WiWWa8C9Olz%nVQ4Ge8+4+LFe-04y;< zVvK?qeAHSz(P{wdOGW_L2Lu!pEC>4hfaGsy?Nyn`}YG!DPui^mpc@FLOA z{MmjVtKA`?pi+Sn23ZAMy7~vqkH00UDJh(S`=*RoTBX&<)&g$lA<%t{xeam5g(iTt zwb>j6eZK-`EKxqprrr2TgJ5@OZt0$QT!#wLZCMWL&LRDUJQ*HuJ!mHTWT78cKpb1>T#Du!Z$k>Za0AK( z9)_D^nzn9`SVHJfr3m1mp^-9FCu-mHUBv(R5N8KMqB3junhpJVSppC(wAGc8EVRS1 zw{~Wq^7Tdt-0MoJHS{R_8HGF_Ic}Xj9~(*0&I!6qEt${2S*bm#!%>Qdg@OSDqZ{s^ z#5ArZbSm6$28C_u#`7080&njl3k6Qb(@O^mzon`HtCTg5ki6_dlA{?he-@!5mJ9Vs zF)`k7RvFk28$HrsS{7Uw_c#Ur7KX}E!h$$v&Nr&M(2<0h;_sK{YgZp<7wh|RpJK&C z)r)MX<<=^-WvL4fwTUW&qR)uB8poJ&nX27)u>ZQ4!XJgGEG<37%Yu;3>LWDMVt~<^t!yRx+!1S z*5q&pBuZjJ{x-t?p_Q%vV*WVPdWKX(VZ<&(DMp=}n4u3LL zK-pq~?_=s4M=gFb&E_Ku?hIpMz62GjPRF7QDS|L$_$?bj zLqs+FjL~1oF-}7tGSWh@xLPXr8r`df5ZiJfrvexVFG)9N+T}|J?=5iNe~FKjT&}uV zth}mha2<_%W_fEwr77@oH(I_eBTYj{2V!veHsDfLrlD7mff+~a$)hFHWd61RK`=Q` zp1iUoLK}@;ml@yQ4$<6fQpTK(tw1p*8hhd~9pMkjwtF1cM6g|r2>piARC5njySoAPp_ z5*Vuh!!KrqP>YPMv$wa*?agWixF!CU3Nc8MR78OQYtppPxqL4CiOtn9!CflW`{R0K zd&VSW{|t{sj(GR}8&L~7`Mj``^9W%Ht+*C%|JGRPA3aAFv0N-W3zE*Zn~}Hqe#eY=b$q@~#rRONoRuYL`V;{4Uue~K{cY`+w4y9+5vtNsS#)f-^cgJK*> z^tB+57!$4XfCtZ(=v;X!HJs$m8&aRZLLy89Sws4$~ZY{pl5$odssx%HV{Qtg1lEYa zpkp{Ly0UGy++mAUzt!W~KCW`G*>v5X*=>9*MMj$Wv0(9eix1vQC1#*?|gDr;jcPi6k} zH|{`qPTb~?7k%I5qzMhkN4!Ik*!>ljiCCqSQDmNaFrtKjN-@d%db}>)W}U8Ve9v1u9-xAvx*`-V?A$o_M`{!@z-^i>NWwwFuTij7OPFoi z_4ua@WbuRKvL~yqCS`zr!>yjMn6|ZKg~Vf`S|NR*$nnR7k|ho~e;OM8+=JL36EJN# z@$#}Y<30UX;j*5?XGj^JRqWF9a|;M*^a!%5F>GIQq=K3TNkp{Qj|0gPi_tH8itdA3C&MF))bFx7STd5-#tg*wPwHl zr0U}${pFmeX^3(C-Ck3ZN2k{Z*2k-#a)VXH!`J6nkA|g>d8B{fhkSn64XQd*9ZgTl19LTyy56)-6}K zub~^4fxo^K=?^I2NCe;z5LJn3Xs++w8{6YgY0RH3*W5qiFYJH(*{?Sw= zFmNt~?j^$edjuHbuIFBVe_)e|G~y|^9)MhrF7CU6Bb;vwgv>Za(w7Sh|@ChvF%8pfU6D z;Q%YY&r8vpA0$Pg7R&?dskx68xaw!{UO4c&A1f?8C<}Paee&?7mqimeqfe6B>?Sjn z`Xi^P0_7(m;~nPN>DUyXfGglDJc^imrkmQnjw^7R_3--ku*lzpXoKW^YJ~P=@$?5$9l$R|xjz&_S?iP3B&D^LG79?5qG>3Y=W7)4Gd%`lDl*%aXByXm;OOeAZ z_j|w!7n$yZcY9+q0Zb8!x%xJrjj<^HtbGw*GLUR&K%UOtbso~avf^8NM9$pgQ+=?HuD2gWFH-bVx12W914PyDiEF1X@6+ZO4u0iEv zARz(wV>cAH6Ec5d34E4~X4`+)j)aOA~Dg>nO#nSltW0T?Wm{mZSs<;v#wF?jb&pf|trDi8Minq8bzgx1V=$@l3=OMPMgCZ%9 zzHK-3J1|>>oak`ncUXIR>QxlQi#`78dhFMJEoF-Ir5|(N)C71bvB+yO0gCJQ8Wqeb zZ)SHK%VO#i>+7W-=`3X@*tB>){gdGgT==eI?>vmahiC!+yd&CUiFHGM#(R2jT(Z|iqmI-->!U4s7xV{Tky zLN;$?qsGc$0`FILNvj?Q6vfccgm0hu$n+Ww4TF}fEIDnDP`8>G=({!*VjM6Ibvl3& zXd59Fu0iUK#vc)RFwOw0Pv1zCKgsNN2KV+^QYF-RJa=5RK*&K9GaxgDC{t-lKg&&0 zqSXhes@LVu-b8efk+Er!H63}jh((dY>Z4T>>+S37-ifSc2Zrc#o&|U~h}(VdjjiH9 zy}D_|{1Zw;pZW)ZITR& zhd&{U$*FIchuCVCt*B^t`(}00*=26w0iolW@CL@{1(JI(7Ja;OS#ua-oX*-9J;}bW zoc*K4QX-y!f`e<)ugeT}o*gRzu0&@bV61JD?Jn1w`eeNEm?`4xA7pjvEf?CI9);*gy-1{8&<SXFwRt<=_CZlbmNWBC5eeTeHEa||V2M&C{@ryfsx!~FAS zO8W^))~>*7lcipvmFN9oclU6}#_MQdY>YxnO>Gm1kwh2)xqdn4a1yyXts4L`Mqu9* z2G0gZn`NICQ*{@XDmqwE@cL@7MmWG(5e};`{tWw!w$Xa4iGnjOH0 z5ox+J7TKkigzhJB-X<>S?fX89rC@^Hpea_R9p2wVO4PdMOmlBl-N7Dh(kJC@Qj8-_ zP7p9&5#^Cqf;rtnd05fDN+D6&F?VIpiD zTg@y=?-5oDJ{qdd&-9m@&cO6Mh6kN;kgOoQ_ zG)f9bWwhoIv8t6fhD&OC)MWA9OiILhY0A1>pr${BB?a}sczUw#tX=#aKU>M(<-R|# zez_9h@w|m649K{}-P`-xj4I&G|8+4rG+@6deb~H=iORh1F9!LYHyVFAJ&hIkxTVlI zO6z8Ck7VS-6UnRb4MNL>(iGtWd9f+VNZ8NaaGKd2J6#M14u}}kOcLE6V?ENXV1Vqn zJq2?~oH16(dRXS6Ri!WJPAp3UD;w{vPZZg&A*j09E7)*7Cjceey7q{3asRq^D%8|q z*%)pvE@-=6u1BPKqk$Ytn-!)Fio9zHz-+gY@*E(zvU~ZGG)|=Ir<9li2nl0BxYw3TG@>}pz zuRG0DqDxBEZ?o*A2cf~m0QH!~J@RhnlqCOqwj5=bXBe^=V1 zv0&l`0Jjr+$%4&_F#?sK2jlCNx3?57)$VSjd0*+&rN+3rmWKK}nPNv7E~-AHAwubC zXhDy>sS$tReMg#dU=tF2w)^$ezYXB2A@T2v7p#QkQuE%A(k$z@>VyekA!NavEpD;< zbb_KS9AzSV00miuXxp80nna3mZk`gPaYx})TzZ>G79Up~w@(!7*p2Fd<-{+s){$Cd1( zCyFN(ipmr6aM>OjalFfj>LXor>j0Soey@j9aPR%T#*=SQ)a}Va2d3*c{=IQhR9E8 zHiI^JmWg^CEPqB((`(;*54rw!VWXpyYQH0GUEO5D;J4m;PZt@Ky0_UT#o=TC^IUkA zzV2E?W)<5Z$A$EjZW(A+f^G^M$`{r19mbj-K2G7Nv%LQ;t*W3yVVv7$&>l)^@X>iu z6GEvNClz&Nv;4CfrD_i1d}|w{^H0PXW47^#^d{9p!#@L$iW*yvVz}@7S*G?G!9xdB zL&Sv%*r6ltfk+A_CWtN0*aZRDht0+iN;0Xz@%r_x2#c#Sp~Mjs03Sf~s55sq6g0t8 z)YcY{9~e}t=8E2}2wiu^8?)sQG}?eMqP~{lL&Dj_M>TMaAs_9Rt(F!TZZKxDO?reH zb3&@nqM~em{pgZNU*B?wU^gu^6HZAcK@@`M5rCPS>5FDFS(D!JA5mk7#+j=noyxj? zMsBxYVZTe)dBmc<-cv#p%dzt{CCia2hT6t&Rp0MkBd;qp{+5ep=UTz^#wH6&KEBe? zRr+5t^h~=+ZdwOlQc;?vhHEd8X^72uSTEv^pLft=`4_q5pX!ZcW$vUvYUi*A^`I&l zG`U2+o|0~+C=!zxponT!4xuER&K6Q(!TvNPEYUF|1jRd*LXyIwcEE;aNZIeemOv}# z@gJY09~)f#F!~u`yhu!kR=qd%_stjD_2`bxtHURIQ-q78Ca5fN%fjtU>r4CrA+v;( zFn;jlDa`HRF$36-8W~F9Vqzr+$Uw~UUd(ZT9)Q6@UA;D}!rrc@3ZO^}#b5Y+lqRSK zS;dmab-vFcL`_pNXFeh=$*22-ZYamImXrlrOwbk|KqB}6iDQbuplDhLB&HAxwVxn9 z34JA+ElH}dIrhknDRGj0jjHNW*rTAP!0wxDoFX;eR%Vn)E`kW&Ao_b#i2sf5M3R4i zU#j~&hZ0#v?ylZ>ZkpQ_@5k@%If!LOKCVa;>e`eVI91;x)4=fHK+xh`jPR6Kl(7vNGiM{ASxy3w2`(ur~LV5p)g0{bBC0*4cr< z4hBWM_YOw2FP-^|t(Kn4Y0@mHLRwsuJwV0Uj6D`wlhj~Jbeu6L+QSz`@4$4LMv7KW~K5{FkxVZmW9|HTe(~@;Dl;+8}bKxHu?laM=LZWWE zThJ{T%t?9B|K#!Mb6g&<7YMVtebPKToKttnQOz zjJ%tU6G;Cb0GdE$zoZ`Jbu0uEU;{|Bob?%*cye+ER?!))qoWzBK`oX_xW23gTQ-;Z zi3CIpIup@8u(&t~q@jDV6!+vv$^4stuLcOnFmoh)GW&{(F0HDv2^)-w@{|DOm`hxc zvppc}>Yf+JdR zIppFi+^~eiA-aO|{8OQrfErjX8#49vjbczQX*;{xwJlq@b4wvYMDn00NQaM_8u|}Z z!9Fk!`@on$AZZ^^EIa>xZkA9i2vE8+K@dLtxW8d5X8l&okRDV>#xM1JG6f|ulJGh9 zLqikL^)IZs?{ZRL{$ixCuv&nkLwd-n0p5nN0>IRbUoU={rK}i$@&$0YT23u2yvQ0T z0^}*gWY*AJbE!e52uFk9E3fuzL&IeVXweECP=0ew<&mNwX9q6r&_1vgszev%ajKe`|WgX1n&hGg#I{1p=$`B8!J8Y}V^mH>@laM#&;?m2xOh!ftK zl14M_K~^dNRU$TD*AJ{d`Q(kWBpyVE;=QXXNs&M7w~f}HJ2!;TWaC=b&Mo5m1r#Nf zm``bdi8>-fkX2VViv0z%vzOprJPcrPnb8%_f7O9PcJ8YC%R!<0F1BSNdA*&`^IISW zBcOqB4!=`zn#&`ozGzr4IA&I@%Pun+0sqlH5P%=}OD~<%o_Xe+*wRVlSwo;O zTb0rQziYtw16%MN#2fK$op2>3gOH+Vc5T?qogDOvDG3JcrWbp z*;bO31i->OJNJVD97lL-{}^>*Yz#uz3dq*tdO*N&mMvzYagazt|A9FK&wJ(>Mk8v% zib0cB>Q&Zbg-mP-kw$*su&JpFONI@2Z%}MB;;zMF>ygETMX}1}80IsUWUgegBz%XX z0xLCLki-y?g}8_NJa7!j0M-Z}bJg$n_*nzI za3KM>0gs?q0Vpru4*(pGtvt_CXuHyd9ml5aoK5>fyV5FX3S$PB2^cw+RqAvCJcmp2^gOQ{BLeC!_K8+cl!CRz# zhlI~mETR;JNBt>oJ=h2UUlm<%evMTs+Kj3`?P=BwK$U1&E5nE;2SAP~dQgw9+M^sS zGBUaq)AP&LUO(NZT^t_OcJ1DV)qrw=T)fXbvg4d_Xdf`3hc-bUt%5W#2K&H}fP=1I zS&l@x5#i++QmSBbJ)ht%E87VJUn>|fC9(+Na||NmL1tlraZ}Il6FWcY9`16_M-on=#Z0BBE+&v-QC?{N-HhhIk>!B!gWaK9mTT)sK#yR zprKN!8^S&SwP4lfM_|uWSUNn1wc;f~_z}e8j3Y;wjKJx1V;QOgy6G*@GaDeXGIj!u zQdz5Ks_Q9_qH0vnVn? zDec6Gm#}kh4l13T_2oOVa~p+6YoII&@w0Sp1J)1)1(@#%h+i@%Q7c>FE64?i& zbii*HG&SvnB+v-N?1LD}KqO=Wa+Zp(C((7LeJvV}C2CX=?_=2tcCx!r+)8&ZMX7Mv;4CTL4Ww~Af9YDA)XB=T_`nQ;KQ z?RY!39n%eyd{h~qq);W96~QaTPlhd@pFabnRQn59)3swCU~h6yQaa*#K8>j5<@@lf z2ZD13k6+IR;j<#xUTA9>9v;^Qhfd-P!Pc6dpUkpfe4>;P5cO~#$eOz3`L-MWT(l2Z zh)Q+h#98gBr%s438ZoQ1{xc8H<;$Fjc)t=GQCeDVhc4TJ=Tr;$aS0-RQL#ie;)?xC zPd4eki?1mGHrGLF>ISUZK>YN@kl~8ScnMR9;hgp~#D$!MC+CaW*|V=9^qVf0^$6=X z2JsUXFjxb9Y-No>vX!huZ`ys10SajkB1v}IzYtyKmlPDapaNL%HYbc>?~ko*8qIphafHRT{oC9 z{Q{E1kNA=-aU4@UPxfA-`=T7yRdk-tN=!uMYg zEvRyK=d8sPVFV0RT852AT^O_*Vc4n?OI$?sVi^jfnta($r6Z4pK65?44hGXZz&vjN zOe#Q#Dh{7)W(0Pofa%$@eVFy1!WQo#-!^*+} z{Rhs5*SZNuj^(vxA0R_yU=E*vNp6K>ulN z8KH>DY&r9%BYYH^HgOE#>VdJd!H^}sa#F|zZ;2-xl28w=AYhi*15k#xU)Kkj9FkEt zp7dtjgq#?F+Hwd7i*pD>K!VCUM0nOM#|!C*GrCOXCGnH!Va&&jg7-1l{GtrMiI;NV z@oqZ)s*sil%5T=a^2%xKFRz4J(1Dr0LcA`S1;t<4GR}A;06Mlv|ABFM6`mLUG8mvz z5CSW|niC0}hEVCZUtYdf==jo$RITSrm`EZbBOtS|;F3KJasND4Q7!?|8Q?{V`uh)o*-m`v*+@$&4C^&ALWZ969SW{FC#jo1fjYPi4Pc1Q)Ua~jM506+jq zL_t)ong!3di2p!N(p^luHvWP{O{s_xn|&bOZRp$E;ljHeGdSIGsk2AJfae?s6GXX$ zuSB2LUVix*!0nPCd@jXiGwY|s3xJ=JnZw}+GvY^QByQkKN2nXxHTh45DLj-J=FTr$ zq)!Gn0U3cn>uOESTY%BoZv$2>$w>mxv>rhd2%$A>SxeLKo^1-^BUmVO9j5YbD&kTN zDatY2BGwPv5yWiS7GBdcv<9K!cy3vy=no8;>)Nl{m2E$W;B+e{NXEuS!J$XBj*jhE zI&8pfhRTK5D+z#YATf}6FhXrZ!%j#hjW85m0JE9}vNB!)t>VcIT`b{BNwTd0NJ6XV zPXmZ-7t|emdpx`(%Kpkelldyb=XV}fAT^EQ_Zr@%XwJPR{19nsJwLwR0u$qJMqPu+ z2>lsfIMSP%==Loc0N)!K(O{R5Urgq87=@zVj-0O5lN4(rdGkG6t4kDQ@nfKf&O18; z%@ZwVk~AsLGyj`Y)6p2JPl9NE^rJgow5(@tf;fdC3nt~AYQQsZT6Q_(%O}7%>r=F! zMW_Ud`n--#E|(4DEVW#&MSR<{&J&hix%H$l&D5X?(%)0M=| zwo8?XQUYNU1e2V^rkzumfft3I&oRh_;R$f21dfSMY5NW4TUjZyrlyvS)m1s(99!Ne z^Qk~A5d^_q!=XguE;O{{sFJc3**sj|CljH-{ibEz{=(gNKewM{AdDpx@_J- zP0|;DF9m~(eH-lykf^cIK;k)xotr==5xtPaA1~Hn=@fPVw>2@bXe=!32sj*je9))x zRtcz!L|C{ak)7L)U0?zMxU0T~u;r&BWQcySTov^;RBmE)~&h=Lm>MoI=$7l}LSnJQmg zZvgQpMnxG49MX3`+rv=cq)FAbY>qjq0D?P;?z`PBBf8GbNUWc~#84r?6ar+^z-;oW zI!OF57z;~3??n$F+rU{W6!JokS}^MB-nUd)`IdXOZL6Cf9Q^mAON)=gDEOlhsHTd-o#0?9zR6-ziNSn(YODy^<=*UG%L+APAB zFE0-XSYG}>B%Hw`8+J-dcR-@*fY{HKq2%lNR1vsMcW?lKSVqr?zR<0jl0dfNzJe`} z9Gk?64@yD~$C6oE^3xYuTMxZuadEO)*PrU0n)+sWC{zwySOPzy)FI0u&XD-Y1h_&_ zhAv4dF;zW@YtQFy;x0*v5wSVZIF{r%tiwXv~dUz?eE5n;|e zU;vE&w?3)z)<4PQjx_o?M6_ZZuB?ObBpgJXg0(} z%1Q%fPJX3k5)4Hd+cwrdAoYA7#~%dW40v{diNUDC)`DrV*`n#%AD>rmOV)#IP0xX7Gn&l04CKb4mAj_5ps~_t)3oKZ?AgFTeb2XDTbT{o~_?wYaE3 zq>f06ERPcjkE6&)UO9Z-hh5HP$@-kG772&#Z5~6;CjBUm!J)|W1IAw0Lxlb4AGsNcEZ%b zrnCLZzHtdlFvpHP3-?_{l;$>Ft_IN8U#UO2wLJ`?6fcJY7cg$A|Mkaknl|MAO!#4X|#83CUuTuV`5-j9;^NvKr#7GNuujyuIRU`30$ zRGKrzc89kG2LdH$_!S}K+0t>0eAWiC2UpZHx=@8*>H7r8B2z*XwN-&3N!qo zARH-{MmIxB5WG(VTT|0*(6x7=LW<911RTYrA=_++6f2k!MK0)fR3?t|a1oll|fBpTRW*ZM4 zl%icAU^s~zm>6^te`~A!#WMop672$9BIGIoeMq9!N|ss3l)6tsnle{cNpqF2vtC7u zxwIi?OnK-bSzTRS(~Pb+EK7n@%W0{blDdPb-;IAt;-`_1#y-ySyk_FS@&^$QpAL^E ze5K1VBZ1i@W`hFw8a@m*4tFv;t#s2qki6O`HnZ4{0BuTd@nX%_W2- zdmaSS52ipz71^m+X@Ik9#6Hm2cqeACEx1?cjYdkvkn8z~1A!UyUf3i$0c)=FlZYj; z6G~)gWC9fT`8C}$XU=IaywD4S=OFs#!8_dilb^Vscr+5<#U`Urd3hOh`07vV`p?gt zIC0PgTm>j?k71OQQYs{TdE|J~DJ&5qVCf`!PBdf!3s5t8%~VJ{04UQWgK+?ms+XYt z&GAgpBG0o;$+0_4N$7NVa=q>*$c@%)g%a z*?y`3d^T0Ju<$*+#a)hv>|@9FJ9WK%rLywY-z+JiUb}*la|RQO%{r0Dv6?QV0k-|b ziIdo9^fJ5{<=2U%eL#xYES#JeJ;8ujAka#v1^ck;@Fu{;BZ!gsA$9l6n!?$e?}T8m zGtCN=btg9L+=jBNWj$+pzHEa;1PP{xV`p=aOb^55&@r)5zxPcdkKg~ zieao3ki$uQ8xj>kf@fV0N7EF~@lMXf&+_xa?A~5!bqWSUr-aknIxr^cLgZd19ggId z1H#Tz2cyW3?>mS@!l&LwqEAQok@7f;k^#)>OG}t$kv{X89_`j!j|X~sG~Y z;Qk{h7t~;)hByp~6qIGJqh3sNDrZ@`E4#qx=CJMfMHtb}K;l^z1Wm#x2B^gTC_>|w zV;biwD|h2@Fw6+p%~V6q=bXU_*q~NfbqmV42jX+HU^3BqKH;GVpAyR=IPt5mz6M?Y zKQLQPhV^{H7oaO}it2LGk^!3lq(8{Z^?$Q#*LR-cTKRzky=Y92c*pO*&ILjS#3CtC zxv3Zd;mze{sszgR#4-|<6R9QfS?2FZ9{|AqBui2>n7GvRcT-_Q`nR=ikq6(=yQCYS zKoJulgV&<6WYqC6j+6Llh~dm11lPr02+T^7=sPhBGchYhC@n$Z8TBK-%FB1a;+8dIO=b3gJjFvq=|IZ=l6=VzH@IHGh& z8cdWF_?4Y==PrOT{VUv^`_M+UO}99Der9myzLbE|=WAPTZoK|eY-@LSk7%?SW4K&b zI~*nK5X67kFXl^Dej-Ld++(6$fGY%uC@&-d$%y5%O#UXTGx58Ol~4)^#1vt`(6hwZ zGQ3Od$s_fK-}Sy~=fUYy0f5ic_)z2`kU1C#$|EfLh{`2&F*Zc$|KUcZJfK|2PnzZN zSWq3t+nKq(enMcvp+kp5To%0LmfwFF+buuOz19$~(@tfn`jN6R89d}{OQPnoAqo5Z z`Eyt{Jc`iQm+&@;lgmR91eP$PTEIZ1C9sEdLX3YC-ftAr0B3Wm1telJ1?jin4j3Fj z-s|uVqs_9*+?}TgUoslL$8RZ&VCp-1^k1>2|0)pDCH6S*ooJ~fgRT`Jixn&{r}|O- z`&+jB?Z~lX8q_+;0Mr)$&`tcRjP7i zuFUeNyz)qcr60fASLJnY?Bgu2@?-$rqPo)3$_d+ULx&NYDW!;+UF!%y0+O>m*I_yP zGm-Oh!v-z<%tI9*`y^&GaQ`XHVy}gHdGA^=4?G~(wOU)>@;l4R*G-vb5G==#Q{P&y zjX$BDk6vqTNCQE1oMQKnfBYH{Zw`b?W=XXm(k@B603hD+cq%Zn?-BL^hof9D0*d>l zSta!Rrl$Qu&nMI1FHHBMJQ6jT49lN^RqTZqo`=okIUr)FXnKC67xFB+U-8c(*KqfB zg`6uZ?dSII|K0z^`}_AppIJ-2;%s&GoYydDQ=zmF1+cYzsZSImppm%+E1^6xae|^W zK&c=d6UeMTvRwI>Og@Cks2x&T>E)LIy2HKzE?3jJeI2s-X?L%;`axYk z6oTO|p`W~EZ7ZSVlPqOgpE3Y8HcGrC+>~8F?LlE#nUVP@lG_L&{46xry}>z$VkGk00U6@@&_uLkFb;u z;l{SE-82X?!<~12^O2R6UB}?RLMh;iVFDyh3mE8dI3a#xU+BmX>;uoh({32jfwB)o zkWUgd83A_|xBIwdG*-@ZSbL6q?D zu0X7><&hMiNci;Wr>KRMB#5XC04WWBs`y-hsNR)FA^?cUnrx4~AGyK|Len@Tj~hQ} z+x!wz057FR9OlozZ7e0TM74lF+6So9zyA6e*awcm=blnxV*3CIesy&MzdpQ6XCv7s z5;T9D*{T1J1B;)3{@-A8c!7u)rRV20%$aqJtYk+2asoFJdPm9Bn1C*%k|N&{StqDnwDmAFgkl|g8jkHpJkx-yeC1|b0C&fD?} z`}X}MC`1e!O3Xdw#Q<2X`dTnHu|Q8|i}10s91{UV&Pkvt#|!FKBz|&L0yK=L0*JiM zC!s732lTZ*xO5~=oI$vRbo=dJ{SVvT{xzr|9LD6bW8z|pdRHNovJcR$*yme?Nb zlUOyOeSp3IR0}vf2J9ZQfG55CAW!-MaNOhjBz)%MM^ayW@dZTY`7z+LB=meTgjCUY z3*AZ8uT08C-XOd;b*HoO3pd~V!G`mlmE0+s1dP zJI&0P0F)GU2tWly+P(~K7qW{43^d&HXV<6-UfG@+6fs<-}%vQ5ts{9ZtHxpgN zJw9`4E7*_l`s=U5$ae%x;3CTKhY_ z$TM~0{azqa5T6U@L`u~pyB-o~i;E^Wr3I0c>(KCYuI9Qo#q%)X#}Pj&kufuQl5;Y2 zN*tzHvW-O?I+WOvflEkSO1kydzZ&lCee3T#og<%JSinA5OgQPPCgQ|1u5|C@`aHHS zY4oFQfN5Z$A4Jg)x8l9n$YX)YDrTSq#;afsWB9qW3m2XfQkeRht}>KmN&u{@5GkAb zAk~B_DopH0mTyeTvFb@ErD2SP+-RDiR*LLM2{0>h4`SgP&XZso;CM4He-)U- zTy7m|fFR_<_YIUNYR#EXmp zDhb2HtZ!qcl7xTr&AY)R$AO?T0H6b%O#tC=fAjqw5oU5kR;Ai8RAbwZV+G?7QG)GX zSE*V`vh4$Zw7fjVWM*Y-jsBuOS;M*DS7=F z7##wEmY}>FPixw<>oeqVCvO!%g(KGW`BB?0C)Wdz(-+GRY6S5)!J#2P2tI&8+pWF# zy$7^6zj+rTR zz3V|b*I-#Nqv|utl(2=_-%Qqixx7c`7{=9i2!j-ZiI^slkdbknKrE=m7{05A_&Lim zAW12VZZ!`QS{x&eRU*hK*l?^7x;;ITCMqP+y%fxys+iwzL-Oj>?zp2v``Mqp9q#6x zSm6r7gOY|=9gHE9rf^&s2$Yqbg+(FvP~g#B$fV}z{Oit7p4=;dT~l+%u+LYr0J=t* z$){}s#54^U`nY2(AOd8*?|t1M{2lPxwLf8J^%8})zvlx4xvJ=N2W@VEAbzNe2JeH_HOdJs20Qm zP37f%N%)cet7nC6UGmbhug?g2%WN|cX#f5#w;cO+;$^?+`efg#tAiDW(ICERO*>F@ zj!e!oZztnT_<`58FbdWHz9ESI9^iUA)Xg_(*I&O)yEqbrdcYv6WW>pFois(-^7|_n z%FFK^W;+$_Wzyzkc~aGw?46kV*gzwox@*_ANvucDh5Hb}qP5(zgVgaSCj(e2*`dAf zeQ(v?`qrJ$_saxg${CqUP);z09b$%0B~4HT$b2x?8X`{k9z|d>0LNcpwq9Ywv&GZBPZa zVloUu8VG<1Bt<_m0G2UNTH~3bJ)Y;9@@4>GaSMGtgm5&K_*NjB5^o`wVkJ9RU%x}U z_uhL1;n&nC!jHs|BhWY#)z??y(^^yo82LmL8q-H)6T~kGIp$H?3pIEc2vnzAra7)p zPydHl2{?SXC+P^VU@k3jS@5pA4!>j=Tfb0VZW0qSk^=a?s+;G+RgZsg4X zSQ!W+tUoV}Nd^$njoC^XpXi5v&pmf*ci!0rD`-67$9I;c>foGPpdWFtlW^%AB!KmU zCY@e9p1p80Vv;kwJWu@h-~O#fG^o1zp8v79*gS6-nJo(@F0FWX*Y`EEx^jIlv(ne+ z%CTR509CES(RfCkt0MtHz2E1XN5^D! zWC#)f3NWB#k>rujY~(3}7toF!H-$x`^($`7O%W^8lnBxAK77Y%p)J|Ma@20IRAu(` za9MCab_@RbgvWE;?|Hp62vVTTEG243W^ZPnE8DuV&7XaXV&BN6K1>LqvNC=vu=%03 zw!fI-vLDAJw&e9!YHUq+phE^@<&vPn!S(rby+d)IAA#Arm0v`X#7vx)N#VM@QglFn z6egEThvSv}j2x&yW?K78TH1Mb?XmzT1!4f*;id!!3_}__q5(^Bh`Xa+d+j{((eXyL zJideKK8^xlme$eH1bwL-WtI={l^8t-*e9o{uItZ zeQvi64rL09LMDYIYfmQcW}7F|x-!j|ZT-pe=T@H?@_4x0Z|9lYZhQJqS$2wLzqqP< z#r4X{vklw{D0cy;z`?k;<1a<~*S(HNN}+%t{4HB(R-%L%|7QF(37eU0M28*(tw@i| z*j%I=ZaJLom-oBnAN z=+BKOyLMeK7)ZP=fJF$#nFhcqtHO8UbbSd@#z}4qgv8P7ME*1-f+9?GK*hm2 z^iOl;zxG=_cJyjQxKjq_V`eAN;r#rJBOJNOw-Sr*3$CB^fd8f8w+Y`-hcisK?FcDH|!gu1!$o!g;t{=Nr8Lqt= zuSJ5X$TFf4HF8cg=a%a8=LfJH$ehu{FDDz^1fDKImG19n4o#EG@A5qNzof9d69@-* zuiHaIe_V6o#AEPMI#C}AxnN?Cb+({m58?)B`}Xa_$2k~4L|R6}@;=(3niDxvQsB(b z4J}VU-3#u>Ad^zvCX7iRc)dUQm)p004xt@1 zgu2q4PduT8fJm?y_ZY{I_n5cdD&xwJj=sO!vQGU;P0gvBCMGPb%Q`Wt*;|!~02`H9 z7elI1B~$ehPMCDWR@RMZN9NR=u1O)?0gVimpqsFZ9tM4M+o&Zng{zg80#T6QA(wBxo@hg>j6Zt{;qa5H^rRbIIt;>YU;ak=4*%dF2fDrJok+ z1epNuQ>x%{Q2b!TU#9~Nbg)oqX%L?Z zA9HOqL8_WEKSuY3_{n9zmX@vB-o4v^k4pECu0x&6Bj0Dg=o|Cwv&Ufg#)U%rsAgD} zfzWjUBN*&D)!lt|Pt>ElPZNr`xs*y#i|;uctuO>aUNKA|$3z2(&*|;N=sxGp4dY4y zgin~_ujO5Jf5(n?F;h`;P>^Ez@pZ&st5OmjrGthEg5X|q&6=Ng2269fuB7C-p2^8? zo`313mjCy}iM>rE{tFj=b?fNpU4Q9xJo9=@&B%S&Pj4+PInWr>!IcdLgJ8@8p_U}7 zEXBQS+MJ@8EBaABeI!g(TnJkL7@zuFa)(w{=0cT~&)w^GfBX517w>p%VBizg9IF#2 z{$S71qX#}82>jbwulMX{4C8FW!U9G)Xj&X=;tz?oP>EK2Czk29i)BAmYBWAapT|Bw ze|{9q0~2AyI<7JZMgpo5+kl~_4)KAawY5xnoD>WK*#8WmGAP1QKLG)Lz1HLCfhVDG zo9e*8S!@F0=do!ESj!sK6?KoClu|$l5<@9G1owkuGNKKoJxTm3r@9so()@v6BFc3N zdj#DzYijt>i&p4at4{1u03$FE;%uz4vU2=W!Qjc00|Wd2uC(+=Cu(b-{*(06c}n<|V>SCHFNE+Q?$6CF!8ksIiH#eu#GJYI zl7txl~2 z66aDt{5$w$@mU8aMv)>Nk4A2CIvVpB6-n|z= zE}jNJOuWZUEU|ATzLJD*Sw3+8AO@@j_3l8>1`Zm_k#Y1J8rng`4QSgi7{PHJ5s96) z-)UdC;KDcNf@WEnMT>I6I@u0NacylHf@&mgMY_Aua7vdq*XOK5LR6NJ8T@jqE#I=$ z`*n4au+()-YtR`J8Yh^8b9B_Srly>nO0h|3)k$e{`}P|^@O6;TVClrF634o_x&puJ z!K~Xvi1meqSs}u6QjscVObv-IP%p)HGJfv%qf|XgH|K`0whp5s)fhP&Mc2e4I76w(#f%Z3i$O3N zPSNET>RfacV}bu7srq=I=T}N%duaHp#)=w4!x6TL5*$r2E{wg$$i?szwpZ~I7;`zca-5Y)$zy$e4Wz)EL5x#SOV7igy99!yS|BHdMD_8Wgqd9StF3JW4x^E4 z#2*dB*mLliLa4cO~B0j4Vt&Cb(~KXt;Ofe<+@-D;N;1}q8Av5 ze(2O(OGnt{_2?u4KmgKL+AU(waS3GS&UUn)BTRrqCl(vI$M-lmJ9l;p2Ed%j5JL_k zWB&OBA$D}!sx>w>YfjWF313FemBi1I6oZmd1^1b^x9@~R&;$hO!?)nh2t*46JV3}0 z2wi$L#@Ek2wzft-VnRf|QWKk2to9KO`6Rwi#*WZe>lqWCk?=YDPT+ekGc%GHDS
    + +
    Kiri:Moto is Loading
    + + +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + Kiri:Moto +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + +
    +
    +
    + + +
    +
    +
    +
    + + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    简体中文
    +
    dansk
    +
    deutsch
    +
    english
    +
    español
    +
    français
    +
    polskie
    +
    português
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    loading
    + + +
    +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    mirror selection
    +
    + + +
    duplicate selection
    +
    + + +
    choose a face to place
    on the work surface
    +
    + + +
    choose focal point
    camera orbit center
    +
    + + +
    render objects solid
    +
    + + +
    render objects wireframe
    +
    + + +
    render objects transparent
    +
    + + +
    trace edges
    +
    +
    +
    + +
    + + +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    0
    +
    +
    +
    +
    +
     
    +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    + +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    index
    +
    lathe
    +
    laser on
    +
    laser off
    +
    flip
    +
    register
    +
    drill
    +
    level
    +
    trace
    +
    gcode
    +
    rough
    +
    outline
    +
    contour
    +
    pocket
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    + + + + + + + + +
    + + +
    +

    this site uses cookies to preserve application preferences

    + +
    + + +
    +
    +
    +
    +
    + + +
    +
    +
    alerts
    alerts
    alerts
    +
    +
    + +