Commit graph

46 commits

Author SHA1 Message Date
caf303d3ed Add checkout.py: writable working-tree checkouts from bare cache
Implements ifcurl-5oo. get_checkout() ensures the bare cache is current via
fetch_ifc(), then clones it locally to <cache_dir>/checkout/ (no network),
and places it at the resolved commit as a detached HEAD — forcing branch
creation before any commit. Viewer artefact patterns (*.ifcview, *.rdbview,
etc.) are appended to .git/info/exclude so they are locally ignored without
touching the repo's own .gitignore. When a newer commit is requested, the
checkout fetches from origin (the local bare cache) using the default refspec
to avoid git's "refusing to fetch into checked-out branch" error. Unblocks
the JSON-RPC connector (ifcurl-242).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 23:38:13 +01:00
988eed5a93 Add discover.py: find local repos matching a project by genesis commit
Implements ifcurl-egi. find_local_repos(genesis) scans directories
from [repo_search] paths in ~/.config/ifcurl/config.toml, computes the
genesis commit of each git repo found, and returns paths whose genesis
matches. Results are cached in ~/.cache/ifcurl/genesis_repos/<genesis>.json
for IFCURL_REPO_CACHE_TTL seconds (default 3600). Recursion stops at repo
boundaries and hidden directories; depth capped at 4 levels. Adds tomli
as a conditional dep for Python <3.11. Unblocks origin remapping (ifcurl-oef)
and ambiguity resolution (ifcurl-77u).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 23:05:33 +01:00
8625528514 Add get_genesis_commit: root commit hash as project identity key
Implements ifcurl-4d4. _compute_genesis() runs git rev-list --max-parents=0
HEAD to find the root commit(s); get_genesis_commit() wraps it with a
per-remote disk cache (~/.cache/ifcurl/<hash>/genesis_commit) so the git
command only runs once per repo. For multi-root repos the lexicographically
smallest hexsha is chosen for a stable result. Local repos compute fresh
on each call. Exported from __init__. Unblocks local repo discovery (ifcurl-egi).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 18:32:54 +01:00
ae143b74b7 Add fetch_ifc_path: materialise IFC blob to stable per-commit cache path
Implements ifcurl-i07. fetch_ifc_path() fetches the IFC file and writes
it to ~/.cache/ifcurl/<url-hash>/<hexsha>/<basename>, returning the path
alongside the is_stale flag. Content-addressed by commit hexsha, so the
file is never rewritten once present. Works for both local and remote
URLs. Underpins the connector and checkout layers (ifcurl-5oo).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 18:26:11 +01:00
10ea3ca071 Fix license headers: correct copyright owner and add SPDX identifiers
JS files had wrong copyright owner (The Forgejo Authors → Bruno Postle).
Python files lacked SPDX-License-Identifier: LGPL-3.0-or-later.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:07:23 +01:00
df11dcc9d3 Fix: exclude IfcSpace and IfcAnnotation from default render
These element types produced large grey boxes obscuring the building in
renders of models that contain space volumes or annotation geometry
(e.g. 2D floor-plan drawings stored as Body CSG). Other IFC viewers
(Bonsai, web viewer) suppress these types in 3D views by default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 00:13:51 +01:00
03782ec9f7 Fix fetch-on-miss for PR commits: catch ValueError from GitPython directly
GitPython raises a plain ValueError from _parse_object_header when a commit
hash is missing, not BadObject, so the previous message-based retry guard
("not found in repository") never matched. Broaden the except clause to catch
(ValueError, BadObject, BadName) and drop the fragile string check. Extract
the fetch logic into _fetch_all_refs helper and apply the same retry to
diff_text. Add tests covering _fetch_all_refs, the retry-on-BadObject path,
and the no-retry-for-local-transport invariant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 08:56:07 +01:00
3937de2974 Fix PR diff early return and fetch branch commits on miss
ifcurl.js: URL-pattern cases (PR diff, commit page) now run before
the DOM-based file-view case, which also matches on Forgejo diff pages
and was causing an early return that skipped PR diff rendering.

git.py: fetch_ifc now retries with a full ref fetch when an immutable
ref (commit hash) is not found in the cached clone — PR branch commits
are absent from the initial default-branch clone and need a fetch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:12:15 +01:00
Claude
fb20ca320d Enforce spec: validate query dot notation, improve tooltip
service.py: return 400 with a clear message when query= looks like a
bare property set name (starts with Pset_ or Qto_ but has no dot).
Catches the common mistake of writing Pset_WallCommon instead of
Pset_WallCommon.FireRating, giving feedback instead of silent empty results.

viewer.html: update query input placeholder and title to show the
expected formats (Name, Description for attributes; Pset_X.Prop for
property sets).

https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
2026-05-09 15:26:07 +00:00
Claude
6ffc4a2b29 Simplify /clash: single selector, detect collisions within selected set
Drop selector_b — the endpoint now takes the URL's selector= and tests all
matched elements against each other, consistent with the spec's model.
Self-pairs and duplicates are filtered; requires at least 2 matched elements.

https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
2026-05-09 08:57:54 +00:00
Claude
ccf692b168 Add /clash endpoint for geometry intersection detection (ifcurl-k0x)
render_service.py: _sandboxed_clash(ifc_bytes, selector_a, selector_b) uses
ifcopenshell.geom.tree.clash_collision_many for hard-clash detection (mesh-
level interpenetration, touching excluded). When selector_b is None, tests
set A against all other IfcProduct elements. Deduplicates pairs via frozenset.

render_service.py: /clash endpoint wraps _sandboxed_clash in the sandbox.

service.py: ClashRequest model, _clash_via_socket() for socket delegation,
and GET/POST /clash endpoints. URL selector= is set A; optional selector_b
body/query param is set B. Returns {"pairs": [["guidA", "guidB"], ...]}.
Reuses Tier 2 byte cache.

https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
2026-05-09 08:53:51 +00:00
Claude
b560551f3c Implement clash visibility mode and /query endpoint (ifcurl-19h, ifcurl-8rf)
render.py: add _CLASH_COLOR and clash branch in _add_shape — selected
elements rendered in red, non-selected in normal material colours.

render_service.py: add _sandboxed_query(ifc_bytes, selector, query_path)
and /query endpoint. Dot-notation query_path splits into pset/property
(Pset_WallCommon.FireRating) or falls back to direct IFC attribute (Name).
Returns {GlobalId: str(value)}, excluding None and entity references.

service.py: add QueryRequest model, _query_via_socket() for socket
delegation, and GET/POST /query endpoints reusing Tier 2 byte cache.

https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn
2026-05-09 08:46:26 +00:00
Claude
8ab6603a44 url.py, test_url.py: add query parameter and visibility=clash
- IfcUrl gains a query field (str | None) for dot-notation
  attribute/property paths (e.g. Pset_WallCommon.FireRating)
- visibility now accepts 'clash' in addition to the existing three modes
- to_string() serialises both new fields
- 10 new tests covering query parsing, pset paths, combined query+view,
  clash visibility, roundtrip serialisation, and the new spec examples

https://claude.ai/code/session_01CKpkhWhzkzR5K5PDSR8N5F
2026-05-09 07:27:35 +00:00
9afeae5d44 bcf_api: add topic filtering; documents_api: add GET /document-metadata
BCF list_topics now maps topic_status, assigned_to, label, modified_after,
modified_before query params to Forgejo issue API params.

documents_api adds GET /document-metadata/{document_id} which decodes the
document_id and proxies Forgejo's contents endpoint for file metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:24:52 +01:00
63a6b2bfa6 documents_api: implement OpenCDE Documents API 1.0
POST /documents/1.0/document-versions resolves document_ids (base64url
encoding of owner/repo/path) to versioned download URLs. Exposes only
git tags + the current HEAD commit — mirrors CDE conventions where only
released versions are visible, while still surfacing the WIP state.

version_number is the tag name for releases and short SHA for HEAD.
Tagged SHAs are deduplicated so HEAD at a tag doesn't create a duplicate.
Download URLs point at Forgejo's existing raw/commit endpoint.

Auth forwarded verbatim. No Go code, no Forgejo recompilation.
Proxy at /documents/ on the Forgejo hostname.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:27:25 +01:00
9e1a10e0b6 bcf_api: implement BCF 3.0 REST API as Python routes in the preview service
Stateless translation layer over Forgejo issues and comments. Auth is
forwarded verbatim (Authorization: Bearer), so Forgejo enforces per-user
permissions with no service token. Viewpoints are ifc:// URLs stored in
comment bodies; GUIDs are deterministically derived from Forgejo IDs and
are reversible without a lookup table.

Endpoints: GET/POST projects, topics, comments, viewpoints; PUT topic;
GET viewpoint snapshot (proxied through preview service).

Also adds ifc_url_to_bcf_viewpoint() to bcf.py (symmetric counterpart of
bcf_viewpoint_to_ifc_url), and fixes rate-limit state leaking between tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:38:34 +01:00
ee57e812e5 service: add GET /foundation/versions OpenCDE discovery endpoint
Returns the Foundation API 1.1 version document listing BCF 3.0 and
Documents 1.0 API base URLs. Public, no auth required. Derives the
public base URL from X-Forwarded-Host/X-Forwarded-Proto headers so it
works correctly behind an nginx/Caddy reverse proxy with no extra config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:20:54 +01:00
930b8b5e32 bcf: add bcf_viewpoint_to_ifc_url() and IfcUrl.to_string()
Implements SPECIFICATION.md §6 BCF→ifc:// direction: parse a BCF 3.0
REST viewpoint dict (camera, clips, selection/visibility) and apply it
to a base IfcUrl to produce an ifc:// URL string.

IfcUrl.to_string() serialises any IfcUrl back to a valid ifc:// URL,
enabling round-trip parse→to_string for all URL forms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:04:04 +01:00
837a9d10c8 render: fix render_diff — tuple unpack, IfcProduct filter, diff-area zoom
- git.py: unpack (repo, is_stale) tuple from _get_repo() in diff_text;
  was AttributeError 'tuple has no attribute commit' on every render_diff
- render.py: filter removed_entities and _entity_bounds include list to
  IfcProduct only; non-geometric entities (IfcTaskTime etc) crash the
  ifcopenshell geom iterator
- render.py: replace reset_camera(bounds=diff_bounds) with explicit camera
  positioning; pyvista's reset_camera re-expands clipping to all actors
  so the full scene remained visible — now camera is repositioned at the
  default viewing direction but at a distance sized to the diff bounds,
  zooming tightly to just the changed elements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 23:54:35 +01:00
bcf9d58470 service: fix preview pipeline for local Forgejo deployment
- auth.py: catch OSError (not just FileNotFoundError) so ProtectHome=yes
  doesn't crash token lookup — returns None and falls back to anonymous
- git.py: wipe partial bare-clone dir before HTTP fallback so git can retry
  after HTTPS fails (port 443 not available on local HTTP-only Forgejo)
- pyproject.toml: add python-multipart to service deps (FastAPI form data)
- ifcurl-api.service: fix ExecStart path (/opt/ifcurl), set allowed-hosts
  to localhost, add MPLCONFIGDIR to silence matplotlib cache warning
- ifcurl-render.service: fix ExecStart path, add CacheDirectory + MPLCONFIGDIR
- nginx-ifcurl.conf: new file tracking the nginx reverse-proxy config;
  listen on [::]:80 so IPv6 loopback requests reach the Forgejo proxy block

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 23:10:35 +01:00
e66460fb13 service+viewer: add /select endpoint for complex selector resolution
New GET+POST /select endpoint resolves an ifc:// URL selector to a JSON
list of GlobalIds, with T3 caching. The viewer now detects non-type-name
selectors (property filters, Name=, pset notation) and falls back to a
/select round-trip, applying the returned GUIDs via getLocalIdsByGuids().

Closes ifcurl-ov6.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:20:41 +01:00
fe793d2194 service: add per-IP rate limiting and IFC blob size cap
IFCURL_RATE_LIMIT (default 60 req/min, 0=off) — sliding-window middleware
returns 429 before any expensive work is done.  IFCURL_MAX_IFC_MB (default
256, 0=off) — rejects oversized fetches with 413 in preview, bcf, and
render_diff endpoints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:01:56 +01:00
6b6e47ef54 Split service into ifcurl-api + ifcurl-render for RCE isolation
render_service.py: new FastAPI app with /render, /select, /render_diff
endpoints that wrap run_sandboxed — listens on a Unix domain socket.

service.py: imports the three pipeline functions from render_service.py;
delegates to render service via httpx when IFCURL_RENDER_SOCKET is set,
falls back to direct run_sandboxed when unset (single-service mode).

__main__.py: add 'render-service' subcommand (--socket PATH).

pyproject.toml: add httpx to service optional deps.

ifcurl-api.service: renamed from ifcurl-preview.service; sets
IFCURL_RENDER_SOCKET, adds AF_UNIX to RestrictAddressFamilies.

ifcurl-render.service: new hardened unit — AF_UNIX only, ~execve
~execveat blocked, no credentials, separate ifcurl-render user.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 10:00:33 +01:00
6e62f9e51c git.py: log warning and return is_stale flag when mutable-ref fetch fails
When a branch/HEAD fetch fails (network error, auth failure, server down),
_open_remote now logs a warning rather than silently passing.  A boolean
is_stale propagates through _get_repo → fetch_ifc so callers know the data
comes from a stale local cache.

The service adds X-Ifcurl-Cache: stale to /preview and /render_diff responses
when stale data was served, so Forgejo or a browser viewer can surface a
warning to the user.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 09:05:13 +01:00
a6206a459e style: ruff --fix + black across ifcurl/ and tests/
Import sorting, quoted type annotation removal, and black reformatting.
No logic changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 08:34:56 +01:00
4b32726f87 render_diff: auto-fit camera to changed elements bounding box
When no explicit camera is given, compute the combined bounding box of all
changed elements (added/modified from head model, removed from base model)
using quick subset iterators before pass 1 renders.  Pass
plotter.reset_camera(bounds=...) so the view zooms to the area of change
rather than the whole scene.  Falls back to reset_camera() (whole scene) only
if no changed elements have renderable geometry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:56:53 +01:00
96aa1a2377 diff rendering: two-pass IFC diff with green/blue/red colouring
Implements the ifcgit/Bonsai diff algorithm for the preview service:

diff.py: step_ids_from_diff() parses git diff text (regex on +#NNN= / -#NNN=
lines) to classify step IDs as added/modified/removed.  expand_step_ids()
walks IfcShapeRepresentation, IfcObjectPlacement, IfcPropertySet, and
IfcTypeProduct relationships to promote changed sub-entities to the parent
IfcProduct that is visually affected.

render.py: render_diff() does two passes over the same camera.  Pass 1 renders
the head model with green=added, blue=modified, ghost=unchanged.  Pass 2
renders only the removed elements from the base model in red, using the camera
auto-fitted to pass 1.  PIL composites the passes: wherever pass 2 is not
white background it stamps onto pass 1.  Moved elements (modified) appear once
in blue with no doubling artefact.

git.py: diff_text() runs git diff against the cached bare repo clone and
returns the raw text for diff.py to parse.

service.py: GET+POST /render_diff endpoint.  Fetches both commits, gets
diff text, runs the full pipeline sandboxed.  Caches on disk when both refs
are immutable.  Pillow added to render/service extras.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 06:52:01 +01:00
65a62ffc8b sandbox.py: subprocess isolation for ifcopenshell parse + render
All ifcopenshell calls (from_string, filter_elements, geom.iterator) now
run inside a short-lived child process via run_sandboxed().  A SIGSEGV or
SIGABRT in the child returns HTTP 422 instead of killing the service worker.
Configurable timeout (IFCURL_SANDBOX_TIMEOUT, default 120s) and optional
resource limits (IFCURL_SANDBOX_MEMORY_MB, IFCURL_SANDBOX_CPU_SECS).

The T3 GUID cache still works: the sandboxed pipeline returns resolved
GUIDs on selector miss so the parent can populate the cache; on hit the
parent passes cached GUIDs and the child converts them to step IDs.

Tests use a sync_sandbox fixture that runs run_sandboxed synchronously so
mock patches remain visible; four dedicated sandbox tests cover normal
return, exception propagation, segfault detection, and timeout.

Closes ifcurl-vm5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:38:07 +01:00
02d93e68e1 service.py: use ifcopenshell.file.from_string() instead of temp file
ifcopenshell can parse IFC bytes in-memory via from_string(); no need to
write a temp file, read it back, and unlink it. Simpler and faster.
Update README library example to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:19:27 +01:00
19ce25fdc0 render.py: cap worker count; auth.py: document token leak; README: fix example
- render.py: replace bare cpu_count() with _WORKER_COUNT = min(cpu_count, 4),
  configurable via IFCURL_RENDER_WORKERS env var; prevents N concurrent
  requests from spawning N×cpu_count geometry worker processes
- auth.py: document that inject_token() embeds the token in git CLI arguments,
  making it visible in the process list; note SSH as the workaround
- README: replace placeholder comment with actual tempfile.mkstemp pattern for
  loading IFC bytes into ifcopenshell (which requires a file path, not bytes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 16:56:39 +01:00
39a2786872 Private repo previews, BCF import, SERVICE_TOKEN support
- GET /preview now accepts a token= query parameter, matching POST /preview
- Forgejo Go: add SERVICE_TOKEN config field; renderer appends it to <img src>
  preview URLs so a machine-user token can authenticate private repos
- Go tests: verify token appears in preview URL when set, absent when unset
- viewer.html: BCF import via drag-and-drop or Import .bcf button in BCF row;
  parses first viewpoint camera/clips/GUIDs and reloads with the viewpoint
  applied to the current model context
- viewer-url.js, viewer.html: unescape %24 → $ in URL building so GlobalIds
  with $ characters are human-readable in ifc:// URLs
- README: document SERVICE_TOKEN and private-repo preview configuration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 14:20:17 +01:00
01de9fdcac Fix 7 issues from deep review
- pyproject.toml: add bcf to [service] dependencies (ifcurl-9rk)
- README.md: replace bogus GET /viewer row with POST /bcf (ifcurl-ttb)
- viewer-url.js: fix GitLab host detection (includes → startsWith) (ifcurl-3n4)
- git.py: call _evict_if_needed() after fetch, not only after clone (ifcurl-t1k)
- bcf.py: add description param to build_bcf(); markup.bcf now includes
  <Description> when provided (ifcurl-7wg)
- service.py: pass request URL as description to build_bcf() (ifcurl-7wg)
- viewer.html: BCF export reads visibility= from URL and sets
  DefaultVisibility accordingly (ifcurl-sto)
- viewer.html: applyCameraParam now switches to orthographic projection
  and applies scale when scale= is present in URL (ifcurl-4l7)
- tests: 3 new tests for description field; update JS tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 11:57:06 +01:00
dae1435519 Add BCF export: /bcf endpoint, build_bcf(), viewer panel
- ifcurl/bcf.py: build_bcf() builds BCF 2.1 zips with perspective/
  orthographic camera, clipping planes, component selection, and
  isolate visibility mode
- ifcurl/service.py: /bcf POST endpoint parses ifc:// URL, resolves
  selector → GUIDs when present, delegates to build_bcf()
- viewer.html: BCF toolbar button + collapsible title/comment form;
  client-side zip generation via JSZip (esm.sh CDN) so the browser
  does not need service access
- tests/test_bcf.py: 21 tests covering build_bcf() unit behaviour and
  the /bcf endpoint including SSRF rejection and selector resolution

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 09:59:22 +01:00
335349db1a git.py: LRU disk cache eviction; service.py: configurable in-memory limits
Disk cache (bare repo clones under ~/.cache/ifcurl/):
- IFCURL_CACHE_MAX_GB env var sets the max total size; when exceeded after a
  new clone the oldest-accessed repos are removed until under the limit
- remote_url stored per clone entry so ifcurl cache list can show the URL
- _touch_cache() updates mtime on every repo open for LRU tracking

In-memory caches (service.py):
- _T2_MAX and _T3_MAX now read from IFCURL_T2_MAX / IFCURL_T3_MAX env vars
  (defaults: 8 IFC blobs, 64 selector results) — configurable without rebuild

New CLI:  ifcurl cache list | prune [--max-gb N] | clear

Tests: 6 new unit tests for _dir_size, _touch_cache, _repo_cache_entries,
_evict_if_needed. 118 → 124 passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 09:06:56 +01:00
d076196bac service.py: SSRF protection — reject local transport, add --allowed-hosts
The preview endpoint is co-hosted with a specific Forgejo instance, so it
should only be allowed to fetch from that host.

- Local file transport (ifc:///path) is always rejected with 403 — the service
  must not read from the server's own filesystem
- New --allowed-hosts CLI option for `ifcurl serve` takes a comma-separated
  list of permitted git hostnames (e.g. git.example.com or localhost:3000);
  any unlisted host is rejected with 403
- Defense-in-depth: literal private/loopback/link-local IP addresses are
  blocked even without an allowlist (169.254.x.x, 127.x, RFC1918, etc.)
- README and forgejo/README.md updated to include --allowed-hosts in the
  standard deployment invocation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 08:26:37 +01:00
81f022d4af viewer.html: URL input, drag-and-drop, IFC coordinate transform, camera placement
- Replace url-display span with editable input; Enter or drag-and-drop loads
  a new ifc:// URL by navigating to the same page with updated ?url= param
- Fix syncCameraUrl: convert Three.js → IFC world coords (x,-z,y) before
  serialising camera= param so URLs are in Z-up IFC space
- Apply IFC → Three.js transform (x,z,-y) when placing camera from URL param
- Drop viewer.py: viewer is now a Forgejo static file; service.py has no
  /viewer or /proxy endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 21:15:30 +01:00
9e0ef5b41f Remove /viewer and /proxy endpoints from service.py
Viewer is now a Forgejo static file; IFC bytes are fetched directly from
Forgejo by the browser. The proxy is no longer needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 21:06:36 +01:00
c9af45b354 Remove PLAN.md (tasks now in beads), add syncCameraUrl to viewer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 07:18:28 +01:00
038827fcf2 Add Phase 3b web IFC viewer: /viewer and /proxy endpoints
- GET /viewer?url=ifc://... serves a self-contained HTML page that loads
  @thatopen/components v3 from CDN and renders the IFC model in WebGL
- GET /proxy?url=<raw> proxies IFC bytes from Forgejo to the browser,
  avoiding CORS restrictions
- forgejo/templates/custom/footer.tmpl injects a "View in 3D" button on
  .ifc file view pages in Forgejo; deploy to
  /var/lib/forgejo/custom/templates/custom/footer.tmpl

CDN loading required several non-obvious fixes documented in viewer.py:
web-ifc loaded from unpkg (not esm.sh) to avoid the Node.js process polyfill
that breaks the browser ST build's environment check; fragments worker wrapped
in a blob URL for Firefox cross-origin worker compatibility; model.box used for
camera fit before tiles exist; fragments.core.update(true) to flush tile queue.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:04:28 +01:00
cf935a2a49 FIxes 2026-04-22 08:31:24 +01:00
95adb177f6 Fixes for render bugs 2026-04-22 00:22:35 +01:00
502e497d92 Add GET /preview endpoint for HTML img tag use
POST /preview requires a JSON body so cannot be used in <img src>.
The new GET /preview?url=... delegates to the same handler and shares
all caching and authentication behaviour.
2026-04-16 00:43:40 +01:00
778f0004e7 Fix port preservation and HTTP fallback for local Gitea
- url.py: preserve non-standard ports in host field; ifc://host:3000/...
  now correctly generates https://host:3000/... rather than dropping :3000
- git.py: fall back from https to http on clone/fetch failure, enabling
  local Gitea instances running on plain HTTP; inject tokens into http://
  URLs as well as https://; extract _clone_bare() helper
- tests/test_url.py: three new tests for port preservation in host,
  git_remote_url with non-standard HTTPS port, and SSH with non-standard port
2026-04-15 23:50:54 +01:00
7f92cadc50 Authentication and test coverage
- ifcurl/auth.py: token config (~/.config/ifcurl/tokens.json),
  get_token_for_host(), inject_token() for HTTPS URL credential injection
- ifcurl/git.py: thread token= through fetch_ifc/fetch_ifc_bytes/_open_remote;
  authenticated fetch uses 'git fetch <auth_url> +refs/*:refs/*' so the
  token is never written to on-disk git config
- ifcurl/service.py: optional token field in PreviewRequest; request token
  takes precedence over config-file token; get_token_for_host imported at
  module level for clean monkeypatching in tests
- tests

Generated with the assistance of an AI coding tool.
2026-04-15 23:21:13 +01:00
018ee446b5 Preview service and improved CLI help
- ifcurl/service.py: FastAPI POST /preview → image/png with three
  caching tiers (bytes LRU, GUID-set LRU, PNG filesystem)
- ifcurl/git.py: add fetch_ifc() returning (hexsha, bytes); refactor
  around _get_repo() and _read_commit_blob() helpers
- ifcurl/__main__.py: add 'ifcurl serve' subcommand; no-args prints
  full help; all subcommands have descriptions and examples; URL format
  and parameter reference in every help page

Generated with the assistance of an AI coding tool.
2026-04-15 21:46:50 +01:00
7e83c00487 Python core library and ifcurl CLI
- URL parser (IfcUrl) handling all ifc:// forms: SSH, HTTPS, local;
  all ref types; camera, selector, clip, visibility parameters
- Git fetcher using GitPython with OS-appropriate bare-clone cache
  (platformdirs) and fetch-on-mutable-ref behaviour
- Renderer adapted from ifcquery: explicit camera, clipping planes,
  highlight/ghost/isolate visibility modes, type entity support
- ifcurl CLI: `ifcurl render "<url>" [-o output.png]`
- LICENSE file (LGPLv3+)

Generated with the assistance of an AI coding tool.
2026-04-15 21:24:35 +01:00