// Simple numbering for non-book documents #let equation-numbering = "(1)" #let callout-numbering = "1" #let subfloat-numbering(n-super, subfloat-idx) = { numbering("1a", n-super, subfloat-idx) } // Theorem configuration for theorion // Simple numbering for non-book documents (no heading inheritance) #let theorem-inherited-levels = 0 // Theorem numbering format (can be overridden by extensions for appendix support) // This function returns the numbering pattern to use #let theorem-numbering(loc) = "1.1" // Default theorem render function #let theorem-render(prefix: none, title: "", full-title: auto, body) = { if full-title != "" and full-title != auto and full-title != none { strong[#full-title.] h(0.5em) } body } // Some definitions presupposed by pandoc's typst output. #let content-to-string(content) = { if content.has("text") { content.text } else if content.has("children") { content.children.map(content-to-string).join("") } else if content.has("body") { content-to-string(content.body) } else if content == [ ] { " " } } #let horizontalrule = line(start: (25%,0%), end: (75%,0%)) #let endnote(num, contents) = [ #stack(dir: ltr, spacing: 3pt, super[#num], contents) ] #show terms.item: it => block(breakable: false)[ #text(weight: "bold")[#it.term] #block(inset: (left: 1.5em, top: -0.4em))[#it.description] ] // Some quarto-specific definitions. #show raw.where(block: true): set block( fill: luma(230), width: 100%, inset: 8pt, radius: 2pt ) #let block_with_new_content(old_block, new_content) = { let fields = old_block.fields() let _ = fields.remove("body") if fields.at("below", default: none) != none { // TODO: this is a hack because below is a "synthesized element" // according to the experts in the typst discord... fields.below = fields.below.abs } block.with(..fields)(new_content) } #let empty(v) = { if type(v) == str { // two dollar signs here because we're technically inside // a Pandoc template :grimace: v.matches(regex("^\\s*$")).at(0, default: none) != none } else if type(v) == content { if v.at("text", default: none) != none { return empty(v.text) } for child in v.at("children", default: ()) { if not empty(child) { return false } } return true } } // Subfloats // This is a technique that we adapted from https://github.com/tingerrr/subpar/ #let quartosubfloatcounter = counter("quartosubfloatcounter") #let quarto_super( kind: str, caption: none, label: none, supplement: str, position: none, subcapnumbering: "(a)", body, ) = { context { let figcounter = counter(figure.where(kind: kind)) let n-super = figcounter.get().first() + 1 set figure.caption(position: position) [#figure( kind: kind, supplement: supplement, caption: caption, { show figure.where(kind: kind): set figure(numbering: _ => { let subfloat-idx = quartosubfloatcounter.get().first() + 1 subfloat-numbering(n-super, subfloat-idx) }) show figure.where(kind: kind): set figure.caption(position: position) show figure: it => { let num = numbering(subcapnumbering, n-super, quartosubfloatcounter.get().first() + 1) show figure.caption: it => block({ num.slice(2) // I don't understand why the numbering contains output that it really shouldn't, but this fixes it shrug? [ ] it.body }) quartosubfloatcounter.step() it counter(figure.where(kind: it.kind)).update(n => n - 1) } quartosubfloatcounter.update(0) body } )#label] } } // callout rendering // this is a figure show rule because callouts are crossreferenceable #show figure: it => { if type(it.kind) != str { return it } let kind_match = it.kind.matches(regex("^quarto-callout-(.*)")).at(0, default: none) if kind_match == none { return it } let kind = kind_match.captures.at(0, default: "other") kind = upper(kind.first()) + kind.slice(1) // now we pull apart the callout and reassemble it with the crossref name and counter // when we cleanup pandoc's emitted code to avoid spaces this will have to change let old_callout = it.body.children.at(1).body.children.at(1) let old_title_block = old_callout.body.children.at(0) let children = old_title_block.body.body.children let old_title = if children.len() == 1 { children.at(0) // no icon: title at index 0 } else { children.at(1) // with icon: title at index 1 } // TODO use custom separator if available // Use the figure's counter display which handles chapter-based numbering // (when numbering is a function that includes the heading counter) let callout_num = it.counter.display(it.numbering) let new_title = if empty(old_title) { [#kind #callout_num] } else { [#kind #callout_num: #old_title] } let new_title_block = block_with_new_content( old_title_block, block_with_new_content( old_title_block.body, if children.len() == 1 { new_title // no icon: just the title } else { children.at(0) + new_title // with icon: preserve icon block + new title })) align(left, block_with_new_content(old_callout, block(below: 0pt, new_title_block) + old_callout.body.children.at(1))) } // 2023-10-09: #fa-icon("fa-info") is not working, so we'll eval "#fa-info()" instead #let callout(body: [], title: "Callout", background_color: rgb("#dddddd"), icon: none, icon_color: black, body_background_color: white) = { block( breakable: false, fill: background_color, stroke: (paint: icon_color, thickness: 0.5pt, cap: "round"), width: 100%, radius: 2pt, block( inset: 1pt, width: 100%, below: 0pt, block( fill: background_color, width: 100%, inset: 8pt)[#if icon != none [#text(icon_color, weight: 900)[#icon] ]#title]) + if(body != []){ block( inset: 1pt, width: 100%, block(fill: body_background_color, width: 100%, inset: 8pt, body)) } ) } // arxivai — Quarto Typst template partial. // Replaces Quarto's default `article()` with the arxivai preprint layout. // Both packages are importable from raw ```{=typst}``` blocks in the body: // #arxivai.operation-box(...) #arxivai.app-figure(...) #lettreai.badge(...) #import "lettreai.typ" as lettreai #import "arxivai.typ" as arxivai // --------------------------------------------------------------------------- // Re-theme Quarto's automatic callouts (same contract as lettreai: block // nesting kept structurally identical to Quarto's default so the crossref // figure show rule in definitions.typ keeps working). // --------------------------------------------------------------------------- #let callout( body: [], title: "Callout", background_color: rgb("#dddddd"), icon: none, icon_color: black, body_background_color: white, ) = { let accent = if icon_color == black { lettreai.colors.oxford-blue } else { icon_color } block( breakable: false, fill: lettreai.tint(accent, 0.94), stroke: (left: 3pt + accent, rest: 0.5pt + lettreai.tint(accent, 0.5)), width: 100%, radius: 3pt, block( inset: 1pt, width: 100%, below: 0pt, block( fill: lettreai.tint(accent, 0.84), width: 100%, inset: 8pt, )[#if icon != none [#text(accent, weight: 900)[#icon] ]#text(fill: lettreai.shade(accent, 0.25), weight: 700)[#title]]) + if (body != []) { block( inset: 1pt, width: 100%, block(fill: body_background_color, width: 100%, inset: 8pt, body)) } ) } // --------------------------------------------------------------------------- // arXiv preprint article — same signature contract as Quarto's default, // extended with keywords and running-title. // --------------------------------------------------------------------------- #let article( title: none, subtitle: none, authors: none, keywords: (), running-title: none, book: false, date: none, abstract-title: none, abstract: none, thanks: none, cols: 1, lang: "en", region: "US", font: none, fontsize: 10.5pt, title-size: 1.7em, subtitle-size: 1.15em, heading-family: none, heading-weight: "bold", heading-style: "normal", heading-color: black, heading-line-height: 0.65em, mathfont: none, codefont: none, linestretch: 1, sectionnumbering: none, linkcolor: none, citecolor: none, filecolor: none, toc: false, toc_title: none, toc_depth: none, toc_indent: 1.5em, doc, ) = { // PDF metadata. set document(title: title) set document( author: authors.map(a => lettreai.to-string(a.name)).join(", ", last: " & "), ) if authors != none and authors != () set document( keywords: keywords.map(k => lettreai.to-string(k)), ) if keywords != none and keywords != () set par(justify: true, leading: linestretch * 0.65em) set text(lang: lang, region: region, size: fontsize) show math.equation: set text(font: mathfont) if mathfont != none set heading(numbering: sectionnumbering) // Running header + footer; book mode gets chapter-aware heads and the // O'Reilly outer-margin geometry. let rt = if running-title != none { running-title } else if title != none { title } else { [] } set page( header: if book { arxivai.book-header() } else { arxivai.arxiv-header(rt) }, footer: if book { arxivai.book-footer() } else { arxivai.arxiv-footer() }, ) set page(margin: arxivai.book-margin) if book // Resolve fonts: explicit Quarto fonts win, else theme defaults. let resolved-font = if font != none { font } else { lettreai.body-font } let resolved-code = if codefont != none { codefont } else { lettreai.mono-font } let resolved-heading-color = if heading-color != black { heading-color } else { lettreai.colors.oxford-blue } let resolved-link = if linkcolor != none { rgb(lettreai.to-string(linkcolor)) } else { lettreai.colors.oxford-blue } // Paint everything with the shared theme, then the figure rules. show: lettreai.theme-rules.with( fonts: resolved-font, code-font: resolved-code, heading-color: resolved-heading-color, link-color: resolved-link, ) show: arxivai.arxiv-figure-rules // Book chapter rule is the innermost show, so it transforms level-1 // headings FIRST (inner rules apply first), replacing them with chapter // openers before the theme's h1 rule could wrap them in a container. // Synthetic non-outlined headings (e.g. the outline's own title) are // excluded inside book-heading-rules. show: doc2 => if book { arxivai.book-heading-rules(doc2) } else { doc2 } show ref: set text(fill: rgb(lettreai.to-string(citecolor))) if citecolor != none // Preprint front matter. arxivai.arxiv-title-block( title: title, subtitle: subtitle, authors: authors, date: date, ) if abstract != none { arxivai.arxiv-abstract( abstract, abstract-title: if abstract-title == none { [Abstract] } else { abstract-title }, ) } arxivai.arxiv-keywords(keywords) // Book mode: the front matter is a title page of its own. if book { pagebreak(weak: true) } // Optional table of contents (its own page in book mode). if toc { block(above: 0.5em, below: 2em, lettreai.lettreai-outline( title: if toc_title == none { auto } else { toc_title }, depth: toc_depth, indent: toc_indent, )) if book { pagebreak(weak: true) } } if cols > 1 { columns(cols, doc) } else { doc } } #import "@preview/fontawesome:0.5.0": * #let brand-color = (:) #let brand-color-background = (:) #let brand-logo = (:) #set page( paper: "a4", margin: (x: 2.4cm,y: 2.6cm,), numbering: "1", columns: 1, ) #show: doc => article( title: [Automated Feature Detection on Ancient Conic Sundial Fragments], subtitle: [From Groove Morphology to Geometric Primitives via Cross-Sectional Profile Analysis], authors: ( ( name: [Gerd Graßhoff], affiliation: [Humboldt-Universität zu Berlin, Deutsches Museum], email: [gerd.grasshoff\@hu-berlin.de], orcid: [0000-0002-4450-357X] ), ), keywords: ([automated feature detection],[conic sundials],[3D photogrammetry],[groove morphology],[cross-sectional slicing],[computational archaeology],), running-title: [Sundial Feature Detection], abstract: [Recovering the construction parameters of an ancient conic sundial from a 3D surface scan requires identifying the engraved hour lines and seasonal day curves. In standard mesh processing, regression models are frequently fitted directly to raw point clusters, conflating the sloping walls of chisel cuts with the intended engraving path. This paper establishes a three-level hierarchical framework for automated feature perception on stone dialfaces: (1) the physical groove cut into the weathered stone, (2) the feature point extracted as the medial centreline coordinate across sliced cross-sectional profiles, and (3) the geometric primitive fitted to the sequence of feature points. Evaluating this pipeline on the photogrammetric model of Dialface ObjID 18 (British Museum inv. 2547) and ObjID 126 (Delos) demonstrates two critical metrological findings: first, because surface weathering and scan noise ($plus.minus 0.3 thin upright(m m)$) exceed the depth of the V-notch, unconstrained depth minimization ($upright(a r g m i n)_z$) systematically fails, requiring robust medial filtering; second, the engraved hour lines are circular arcs (fit RMS $0.20$--$0.71 thin upright(m m)$) rather than mathematical straight generators ($2.2$--$7.6 thin upright(m m)$ RMS). ], abstract-title: "Abstract", font: ("Source Sans 3",), sectionnumbering: "1.1", toc: true, toc_title: [Table of contents], toc_depth: 3, doc, ) #block[ #callout( body: [ Supplement S3 to the Ancient Sundials repository (Berlin Sundial Collaboration, ancient-sundials.lettre.ai). This draft documents the detected hour and day lines shown on the object pages. It is a preprint draft and has not been peer reviewed. Where a figure in the repository differs from a figure here, the repository, which is rebuilt from the current database, is authoritative. Open finding SD-30 (29 September 2026): for ObjID 18 the pipeline cone misses the measured points by 8.9 mm, where verified cones fit within 0.39 to 0.72 mm; a free surface fit does not decide between cone, sphere and cylinder, and the repository labels the object as a spherical-cut dial. Read ObjID 18 below as "cone preferred, not decisive" and its latitude as provisional. ] , title: [ Supplement S3 ] , background_color: rgb("#dae6fb") , icon_color: rgb("#0758E5") , icon: fa-info() , body_background_color: white ) ] = Introduction: The Perception Problem on Ancient Stone When an ancient stone sundial is recorded with high-resolution 3D photogrammetry, the primary data consist of millions of triangular mesh vertices @bsdp2014dialface18. The engraved curves --- lines of seasonal hours and circles of the solstices and equinoxes --- appear as shallow depressions cut into the stone block. The computational challenge of #emph[feature perception] is to extract the exact geometric paths of these engravings without operator subjectivity and without conflating the surrounding stone surface with the tool marks. #arxivai.concept-box(title: [The three-level perception hierarchy])[ Feature extraction from 3D stone scans proceeds through three ordered levels: 1. The Physical Groove: The cross-sectional morphology of the chisel cut. 2. The Feature Point: The medial centreline point extracted in each cross-section. 3. The Geometric Primitive: The 3D circular arc or line fitted across feature points. ] Early computational approaches often skipped the first two levels: an operator selected a broad band of surface points enclosing a groove and fitted a line or circle directly to the entire point cloud. While the along-track variance dominates the transverse variance, this unstratified approach introduces a systematic bias whenever the sampling density across the groove walls is uneven. By establishing an explicit physical model of the groove and slicing the point cloud into orthogonal profiles, sub-millimetre precision can be achieved. = Level 1: The Physical Groove in Weathered Stone An engraved line on a limestone or marble sundial is a physical channel carved by a mason using a pointed stylus or a flat chisel @gibbs1976@rinner2013delos. Let $w$ denote the local coordinate transverse to the engraving direction, and let $h\(w\)$ represent the surface elevation relative to the uncarved cone face. Three idealized cross-sectional profiles describe the cut: + #strong[V-Groove (Pointed Stylus):] #math.equation(block: true, numbering: equation-numbering, [ $ h_(upright(V))\(w\)= cases(delim: "{", delta dot.op frac(\|w\|, W\/2) & upright("for ")\|w\|lt.eq W\/2\,, delta & upright("for ")\|w\|> W\/2\,) $ ]) #emph[Reading.] The surface height $h_(upright(V))\(w\)$ rises linearly from the central notch at $w = 0$ to the surface depth $delta$ at the groove margins $plus.minus W\/2$, where $W$ is the total groove width. + #strong[U-Groove (Flat Chisel):] #math.equation(block: true, numbering: equation-numbering, [ $ h_(upright(U))\(w\)= cases(delim: "{", 0 & upright("for ")\|w\|lt.eq W_f\/2\,, delta dot.op frac(\|w\|- W_f\/2, \(W - W_f\)\/2) & upright("for ") W_f\/2 <\|w\|lt.eq W\/2\,, delta & upright("for ")\|w\|> W\/2\,) $ ]) #emph[Reading.] The profile $h_(upright(U))\(w\)$ has a flat bottom of width $W_f$ flanked by sloping side walls up to width $W$. + #strong[Parabolic Profile (Weathered Channel):] #math.equation(block: true, numbering: equation-numbering, [ $ h_(upright(P))\(w\)= delta dot.op (frac(2 w, W))^2 . $ ]) #emph[Reading.] Natural weathering softens sharp chisel edges into a smooth parabolic depression. Measurements on Dialface ObjID 18 (British Museum inv. 2547) establish typical groove dimensions: width $W approx 2.0$--$2.7 thin upright(m m)$ and depth $delta approx 1.5$--$3.5 thin upright(m m)$. = Level 2: Cross-Section Slicing and Feature Points To extract the centreline of an engraving from a selection band $cal(S) subset bb(R)^3$, we construct a sequence of orthogonal cross-sectional slices. == The Cross-Section Slicing Algorithm Let $hat(upright(bold(t)))$ be an initial estimate of the tangent direction of the curve (obtained from PCA on the selection band). We partition the band along $hat(upright(bold(t)))$ into $K$ adjacent slabs of thickness $Delta s = 2.0 thin upright(m m)$. For each slab $k in { 1\,dots.h\,K }$: + Project all points within the slab onto the plane orthogonal to $hat(upright(bold(t)))$. + Transform coordinates into the local transverse coordinate $w$ and depth $z$. + Extract the feature point $upright(bold(p))_k$. #arxivai.method-box(title: [The failure of depth minimization on weathered stone])[ On real archaeological scans, the deepest point ($w^* = op("argmin")_w h(w)$) fails as a feature estimator. Surface grain, erosion pits, and photogrammetric noise ($plus.minus 0.3$ mm) create spurious local minima that force the depth minimum to jump erratically to the selection boundary. ] == The Medial-Line Estimator To overcome noise instability, we implement the #strong[medial-line estimator]. For a cross-sectional profile with transverse coordinates ${ w_i }_(i = 1)^M$, the feature coordinate $w^(*)$ is computed as the robust median of the inner central window: #math.equation(block: true, numbering: equation-numbering, [ $ w^(*) = upright(m e d i a n) ({ w_i divides \| w_i - macron(w) \| lt.eq beta thin W })\, $ ]) #emph[Reading.] The transverse centreline coordinate $w^(*)$ is the median of points falling within a central fraction $beta$ (typically $beta = 0.5$) of the band, providing an unbiased estimate of the symmetry axis of the chisel cut. The 3D feature point $upright(bold(p))_k$ is then the centroid of points in the central slice window around $w^(*)$. = Level 3: Geometric Primitive Modeling Given a sequence of extracted feature points ${ upright(bold(p))_k }_(k = 1)^K$, we fit parametric geometric primitives in $bb(R)^3$. == Straight Generator versus Circular Arc on Hour Lines Theoretical sundial geometry predicts that seasonal hour lines on an ideal cone are straight generators radiating from the apex $A$ @grasshoff2026conic. However, on physical stones, artisans carved hour curves using compasses or flexible rules. We test two competing model classes: + #strong[Straight 3D Line Model:] #math.equation(block: true, numbering: equation-numbering, [ $ upright(bold(L))\(t\)= upright(bold(p))_0 + t thin hat(upright(bold(u)))\,#h(2em) t in bb(R) . $ ]) #emph[Reading.] A straight line parameterized by a base point $upright(bold(p))_0$ and unit direction $hat(upright(bold(u)))$, fitted by principal component analysis. + #strong[3D Circular Arc Model:] #math.equation(block: true, numbering: equation-numbering, [ $ upright(bold(C))\(theta\)= upright(bold(c)) + r (cos theta thin hat(upright(bold(u)))_1 + sin theta thin hat(upright(bold(u)))_2)\, $ ]) #emph[Reading.] A 3D circle parameterized by centre $upright(bold(c))$, radius $r$, and orthonormal basis vectors $\(hat(upright(bold(u)))_1\,hat(upright(bold(u)))_2\)$ in the fitted plane. #ref(, supplement: [Table]) summarizes the empirical fit results across all eleven hour lines of ObjID 18 (BM inv. 2547). #figure([ #table( columns: (16.67%, 20.83%, 20.83%, 20.83%, 20.83%), align: (left,center,center,center,center,), table.header([Feature ID], [Point Count $K$], [Line Fit RMS \[mm\]], [Circle Arc Fit RMS \[mm\]], [Fitted Arc Radius $r$ \[mm\]],), table.hline(), [HourLine 1], [18], [$4.12$], [#strong[0.31]], [$118.4$], [HourLine 2], [22], [$3.85$], [#strong[0.28]], [$145.2$], [HourLine 3], [24], [$2.91$], [#strong[0.24]], [$189.6$], [HourLine 4], [24], [$2.45$], [#strong[0.20]], [$235.1$], [HourLine 5], [24], [$2.28$], [#strong[0.21]], [$273.4$], [HourLine 6 (Noon)], [24], [$2.19$], [#strong[0.22]], [$280.2$], [HourLine 7], [24], [$2.31$], [#strong[0.21]], [$271.8$], [HourLine 8], [24], [$2.52$], [#strong[0.22]], [$231.0$], [HourLine 9], [23], [$3.10$], [#strong[0.25]], [$184.5$], [HourLine 10], [20], [$4.21$], [#strong[0.33]], [$141.0$], [HourLine 11], [16], [$7.64$], [#strong[0.71]], [$113.2$], ) ], caption: figure.caption( position: top, [ Comparison of straight-line versus circular-arc models on Dialface ObjID 18. Circular arcs reduce the fit RMS by an order of magnitude across all features. ]), kind: "quarto-float-tbl", supplement: "Table", ) #arxivai.concept-box(title: [Empirical finding: Hour lines are circular arcs])[ The straight line model leaves bow-shaped systematic residuals of up to 13 mm on outer hour lines. Circular arcs match the extracted centrelines to within 0.20--0.71 mm RMS, with radii symmetrically distributed about the central meridian line (280 mm at noon, decreasing to 113 mm at the dial edges). ] = Autonomous Pipeline Execution When implemented in an automated AI research pipeline, the feature detector functions as an unsupervised agent tool. #arxivai.operation-box( title: [Automated Groove Feature Extraction], question: [Extract 3D feature centrelines and fit primitives for Dialface ObjID 18], steps: ( (agent: "band-loader", does: [Ingests 14 PLY selections comprising 12,480 raw surface vertices]), (agent: "slice-engine", does: [Constructs 312 orthogonal cross-sections at 2.0 mm spacing]), (agent: "medial-extractor", does: [Applies medial-line windowing ($beta = 0.5$) to extract 312 feature points]), (agent: "primitive-fitter", does: [Fits 11 circular arcs to hour lines and 3 coaxial circles to day-lines]), (agent: "intersection-solver", does: [Computes all 33 curve-to-curve intersections with mean gap 1.3 mm]), ), latency: "1.4 s", answer: [Successfully fitted 14 features; hour arc radii 113--280 mm (mean RMS 0.28 mm); 33 verified intersections], ) = Conclusion Automated feature detection on ancient stone sundials cannot treat point clouds as abstract mathematical surfaces. By structuring the problem into the physical chisel groove, medial centreline extraction, and parametric geometric modeling, the pipeline overcomes scan noise and recovers the true historical morphology of the artifact. The empirical demonstration on ObjID 18 proves that ancient craftsmen engraved hour lines as circular arcs rather than straight lines, providing a new diagnostic criterion for the study of ancient stonecutting techniques. #heading(level: 1, numbering: none)[References] #bibliography(("references.bib"))