Compare commits
6 Commits
mai/picass
...
mai/kandin
| Author | SHA1 | Date | |
|---|---|---|---|
| 2933bb8662 | |||
| 98fe040364 | |||
| a1de1246e5 | |||
| fee9bc5d26 | |||
| 8df5de193a | |||
| a675c499c3 |
@@ -15,7 +15,7 @@ data
|
||||
|
||||
# Build artefacts
|
||||
bin
|
||||
mcables
|
||||
/mcables
|
||||
|
||||
# Editor cruft
|
||||
.vscode
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,7 +8,7 @@ data/*.db-shm
|
||||
|
||||
# Build artefacts
|
||||
bin/
|
||||
mcables
|
||||
/mcables
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
|
||||
64
cmd/mcables/main.go
Normal file
64
cmd/mcables/main.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"mgit.msbls.de/m/mcables/internal/db"
|
||||
"mgit.msbls.de/m/mcables/internal/server"
|
||||
"mgit.msbls.de/m/mcables/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := envOr("MCABLES_ADDR", "0.0.0.0:7777")
|
||||
dbPath := envOr("MCABLES_DB", "./data/mcables.db")
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
|
||||
log.Fatalf("mkdir data dir: %v", err)
|
||||
}
|
||||
|
||||
store, err := db.Open(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("open db: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
if err := db.Migrate(store.DB()); err != nil {
|
||||
log.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: server.New(store, web.Static()),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("mcables listening on %s (db=%s)", addr, dbPath)
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("listen: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
<-stop
|
||||
log.Printf("shutting down")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -36,7 +36,6 @@
|
||||
<section class="legend">
|
||||
<h2 class="sidebar-heading">Cable types</h2>
|
||||
<ul id="legend-list" class="legend-list"></ul>
|
||||
<button type="button" id="btn-add-type" class="btn btn-tiny">+ Type</button>
|
||||
</section>
|
||||
<section class="tools">
|
||||
<h2 class="sidebar-heading">Tools</h2>
|
||||
|
||||
@@ -271,6 +271,52 @@ function startPan(e) {
|
||||
svg.addEventListener("pointercancel", onUp);
|
||||
}
|
||||
|
||||
// Left-click on empty canvas: ambiguous between "deselect" and "pan".
|
||||
// We resolve by movement — under the drag threshold m gets the historic
|
||||
// "click empties the selection" behaviour; past the threshold the gesture
|
||||
// promotes to a pan (Excalidraw / Figma standard). 3px screen-space dead
|
||||
// zone is enough that a steady click doesn't accidentally nudge the view.
|
||||
const EMPTY_CANVAS_PAN_THRESHOLD_PX = 3;
|
||||
|
||||
function startEmptyCanvasGesture(e) {
|
||||
const svg = /** @type {SVGSVGElement} */ ($("#canvas"));
|
||||
const ctm = svg.getScreenCTM();
|
||||
if (!ctm) return;
|
||||
const scaleX = ctm.a, scaleY = ctm.d;
|
||||
const startClientX = e.clientX, startClientY = e.clientY;
|
||||
const startViewX = state.view.x, startViewY = state.view.y;
|
||||
let panning = false;
|
||||
try { svg.setPointerCapture(e.pointerId); } catch {}
|
||||
const onMove = (ev) => {
|
||||
const dx = ev.clientX - startClientX;
|
||||
const dy = ev.clientY - startClientY;
|
||||
if (!panning) {
|
||||
if (Math.hypot(dx, dy) <= EMPTY_CANVAS_PAN_THRESHOLD_PX) return;
|
||||
panning = true;
|
||||
$(".canvas-wrap").classList.add("panning");
|
||||
}
|
||||
state.view.x = startViewX - dx / scaleX;
|
||||
state.view.y = startViewY - dy / scaleY;
|
||||
applyViewBox();
|
||||
};
|
||||
const onUp = (ev) => {
|
||||
svg.removeEventListener("pointermove", onMove);
|
||||
svg.removeEventListener("pointerup", onUp);
|
||||
svg.removeEventListener("pointercancel", onUp);
|
||||
try { svg.releasePointerCapture(ev.pointerId); } catch {}
|
||||
if (panning) {
|
||||
$(".canvas-wrap").classList.remove("panning");
|
||||
setViewInURL();
|
||||
} else if (state.selection) {
|
||||
state.selection = null;
|
||||
render();
|
||||
}
|
||||
};
|
||||
svg.addEventListener("pointermove", onMove);
|
||||
svg.addEventListener("pointerup", onUp);
|
||||
svg.addEventListener("pointercancel", onUp);
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
state.view.zoom = 1;
|
||||
state.view.x = 0;
|
||||
@@ -2036,12 +2082,18 @@ function onCanvasPointerDown(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No tool armed: clicks that started on a device/frame/io go to their
|
||||
// own handlers (drag / select). Leave them alone.
|
||||
if (e.target instanceof Element && e.target.closest("[data-device-id], [data-frame-id], [data-io-id]")) return;
|
||||
// No tool armed: clicks that started on a device/frame/io/clamp/port/cable
|
||||
// go to their own handlers (drag / select / replug). Leave them alone.
|
||||
if (e.target instanceof Element && e.target.closest("[data-device-id], [data-frame-id], [data-io-id], [data-clamp-id], [data-port-id], [data-cable-id]")) return;
|
||||
|
||||
// Plain canvas click = clear selection.
|
||||
if (state.selection) { state.selection = null; render(); }
|
||||
// Empty-canvas left-click without an active cable draw: start a
|
||||
// maybe-pan gesture. It promotes to a pan once the cursor crosses the
|
||||
// drag threshold; if m clicks without dragging it falls back to the
|
||||
// historic "clear selection" UX. Other buttons fall through (middle is
|
||||
// already handled above, right-click is the browser context menu).
|
||||
if (e.button === 0 && state.cableDrawFromPortID == null) {
|
||||
startEmptyCanvasGesture(e);
|
||||
}
|
||||
}
|
||||
|
||||
function startFrameRubberBand(e, p0) {
|
||||
@@ -3709,7 +3761,6 @@ async function boot() {
|
||||
bindCloseButtons($("#modal-admin"));
|
||||
|
||||
$("#btn-new-project").addEventListener("click", openNewProjectModal);
|
||||
$("#btn-add-type").addEventListener("click", () => openCableTypeModal(null));
|
||||
$("#btn-delete-project").addEventListener("click", openDeleteProjectModal);
|
||||
$("#btn-admin").addEventListener("click", openAdminModal);
|
||||
$("#btn-solve").addEventListener("click", openSolveModal);
|
||||
|
||||
Reference in New Issue
Block a user